PSPersonio.psm1

$script:ModuleRoot = $PSScriptRoot
$script:ModuleVersion = (Import-PowerShellDataFile -Path "$($script:ModuleRoot)\PSPersonio.psd1").ModuleVersion

# Detect whether at some level dotsourcing was enforced
$script:doDotSource = Get-PSFConfigValue -FullName PSPersonio.Import.DoDotSource -Fallback $false
if ($PSPersonio_dotsourcemodule) { $script:doDotSource = $true }

<#
Note on Resolve-Path:
All paths are sent through Resolve-Path/Resolve-PSFPath in order to convert them to the correct path separator.
This allows ignoring path separators throughout the import sequence, which could otherwise cause trouble depending on OS.
Resolve-Path can only be used for paths that already exist, Resolve-PSFPath can accept that the last leaf my not exist.
This is important when testing for paths.
#>


# Detect whether at some level loading individual module files, rather than the compiled module was enforced
$importIndividualFiles = Get-PSFConfigValue -FullName PSPersonio.Import.IndividualFiles -Fallback $false
if ($PSPersonio_importIndividualFiles) { $importIndividualFiles = $true }
if (Test-Path (Resolve-PSFPath -Path "$($script:ModuleRoot)\..\.git" -SingleItem -NewChild)) { $importIndividualFiles = $true }
if ("<was compiled>" -eq '<was not compiled>') { $importIndividualFiles = $true }
    
function Import-ModuleFile
{
    <#
        .SYNOPSIS
            Loads files into the module on module import.
         
        .DESCRIPTION
            This helper function is used during module initialization.
            It should always be dotsourced itself, in order to proper function.
             
            This provides a central location to react to files being imported, if later desired
         
        .PARAMETER Path
            The path to the file to load
         
        .EXAMPLE
            PS C:\> . Import-ModuleFile -File $function.FullName
     
            Imports the file stored in $function according to import policy
    #>

    [CmdletBinding()]
    Param (
        [string]
        $Path
    )
    
    $resolvedPath = $ExecutionContext.SessionState.Path.GetResolvedPSPathFromPSPath($Path).ProviderPath
    if ($doDotSource) { . $resolvedPath }
    else { $ExecutionContext.InvokeCommand.InvokeScript($false, ([scriptblock]::Create([io.file]::ReadAllText($resolvedPath))), $null, $null) }
}

#region Load individual files
if ($importIndividualFiles)
{
    # Execute Preimport actions
    foreach ($path in (& "$ModuleRoot\internal\scripts\preimport.ps1")) {
        . Import-ModuleFile -Path $path
    }
    
    # Import all internal functions
    foreach ($function in (Get-ChildItem "$ModuleRoot\internal\functions" -Filter "*.ps1" -Recurse -ErrorAction Ignore))
    {
        . Import-ModuleFile -Path $function.FullName
    }
    
    # Import all public functions
    foreach ($function in (Get-ChildItem "$ModuleRoot\functions" -Filter "*.ps1" -Recurse -ErrorAction Ignore))
    {
        . Import-ModuleFile -Path $function.FullName
    }
    
    # Execute Postimport actions
    foreach ($path in (& "$ModuleRoot\internal\scripts\postimport.ps1")) {
        . Import-ModuleFile -Path $path
    }
    
    # End it here, do not load compiled code below
    return
}
#endregion Load individual files

#region Load compiled code
<#
This file loads the strings documents from the respective language folders.
This allows localizing messages and errors.
Load psd1 language files for each language you wish to support.
Partial translations are acceptable - when missing a current language message,
it will fallback to English or another available language.
#>

Import-PSFLocalizedString -Path "$($script:ModuleRoot)\en-us\*.psd1" -Module 'PSPersonio' -Language 'en-US'

function ConvertTo-CamelCaseString {
    <#
    .SYNOPSIS
        ConvertTo-CamelCaseString
 
    .DESCRIPTION
        Convert a series of strings to Uppercase first strings and concatinate together as a final 'camelcased' string
 
    .PARAMETER InputObject
        String(s) to convert
 
    .EXAMPLE
        PS C:\> ConvertTo-CamelCaseString "my", "foo", "string"
 
        Return "MyFooString"
    #>

    [CmdletBinding(
        PositionalBinding=$true,
        SupportsShouldProcess=$false,
        ConfirmImpact="Low"
    )]
    [OutputType([string])]
    param (
        [Parameter(
            Mandatory=$true,
            ValueFromPipeline=$true
        )]
        [string[]]
        $InputObject
    )

    begin {
        $collection = [System.Collections.ArrayList]@()
    }

    process {
        foreach ($string in $InputObject) {

            $firstPart = $string.Substring(0,1).ToUpper()

            if($string.Length -gt 1) {
                $secondPart = $string.Substring(1, ($string.Length-1)).ToLower()
            }

            $null = $collection.Add( "$($firstPart)$($secondPart)" )
        }
    }

    end {
        [String]::Join('', ($collection | ForEach-Object { $_ }))
    }
}

function Expand-MemberNamesFromBasicObject {
    <#
    .SYNOPSIS
        Expand-MemberNamesFromBasicObject
 
    .DESCRIPTION
        Retrieve properties names retrieved from Personio API from TypeData definition
 
    .PARAMETER TypeName
        Name of the type to retrieve properties from
 
    .EXAMPLE
        PS C:\> Expand-MemberNamesFromBasicObject -TypeNameBasic "Personio.Employee.BasicEmployee"
 
        Output properties names retrieved from Personio API from TypeData definition
    #>

    [CmdletBinding(
        PositionalBinding=$true,
        ConfirmImpact="Low"
    )]
    param (
        [Parameter(
            Mandatory=$true,
            ValueFromPipeline=$true,
            ValueFromPipelineByPropertyName=$true
        )]
        [string]
        $TypeName
    )

    begin {
    }

    process {

        # Get TypeData from PS type system
        $members = Get-TypeData -TypeName "$TypeName" | Select-Object -ExpandProperty Members

        # work trough members of type
        foreach ($key in $members.Keys) {

            # extract scriptblock from module types.ps1xml
            foreach($text in ($members.$key.GetScriptBlock)) {

                # match property names from Baseobject
                if($text -match "\`$this.BaseObject.(?'attrib'\S*[^)}\]])") {

                    # remove subproperties like "email.value" -> so only "email" will be outputted
                    $output = $Matches.attrib.Split(".")[0]

                    # return result
                    $output
                }
            }
        }
    }

    end {
    }
}

function ConvertFrom-Base64StringWithNoPadding( [string]$Data ) {
    <#
    .SYNOPSIS
        Helper function build valid Base64 strings from JWT access tokens
 
    .DESCRIPTION
        Helper function build valid Base64 strings from JWT access tokens
 
    .PARAMETER Data
        The Token to convert
 
    .EXAMPLE
        PS C:\> ConvertFrom-Base64StringWithNoPadding -Data $data
 
        build valid base64 string the content from variable $data
    #>

    $Data = $Data.Replace('-', '+').Replace('_', '/')
    switch ($Data.Length % 4) {
        0 { break }
        2 { $Data += '==' }
        3 { $Data += '=' }
        default { throw New-Object ArgumentException('data') }
    }
    [System.Convert]::FromBase64String($Data)
}

function ConvertFrom-JWTtoken {
    <#
    .SYNOPSIS
        Converts access tokens to readable objects
 
    .DESCRIPTION
        Converts access tokens to readable objects
 
    .PARAMETER Token
        The Token to convert
 
    .EXAMPLE
        PS C:\> ConvertFrom-JWTtoken -Token $Token
 
        Converts the content from variable $token to an object
    #>

    [cmdletbinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Token
    )

    # Validate as per https://tools.ietf.org/html/rfc7519 - Access and ID tokens are fine, Refresh tokens will not work
    if ((-not $Token.Contains(".")) -or (-not $Token.StartsWith("eyJ"))) {
        $msg = "Invalid data or not an access token. $($Token)"
        Stop-PSFFunction -Message $msg -Tag "JWT" -EnableException $true -Exception ([System.Management.Automation.RuntimeException]::new($msg))
    }

    # Split the token in its parts
    $tokenParts = $Token.Split(".")

    # Work on header
    $tokenHeader = [System.Text.Encoding]::UTF8.GetString( (ConvertFrom-Base64StringWithNoPadding $tokenParts[0]) )
    $tokenHeaderJSON = $tokenHeader | ConvertFrom-Json

    # Work on payload
    $tokenPayload = [System.Text.Encoding]::UTF8.GetString( (ConvertFrom-Base64StringWithNoPadding $tokenParts[1]) )
    $tokenPayloadJSON = $tokenPayload | ConvertFrom-Json

    # Work on signature
    $tokenSignature = ConvertFrom-Base64StringWithNoPadding $tokenParts[2]

    # Output
    $resultObject = [PSCustomObject]@{
        "Header"       = $tokenHeader
        "Payload"      = $tokenPayload
        "Signature"    = $tokenSignature

        "Algorithm"    = $tokenHeaderJSON.alg
        "Type"         = $tokenHeaderJSON.typ

        "Id"           = [guid]::Parse($tokenPayloadJSON.jti)
        "Issuer"       = $tokenPayloadJSON.iss
        "Scope"        = $tokenPayloadJSON.scope

        "IssuedUTC"    = ([datetime]"1970-01-01Z00:00:00").AddSeconds($tokenPayloadJSON.iat).ToUniversalTime()
        "ExpiresUTC"   = ([datetime]"1970-01-01Z00:00:00").AddSeconds($tokenPayloadJSON.exp).ToUniversalTime()
        "NotBeforeUTC" = ([datetime]"1970-01-01Z00:00:00").AddSeconds($tokenPayloadJSON.nbf).ToUniversalTime()

        "ClientId"     = $tokenPayloadJSON.sub
        "Prv"          = $tokenPayloadJSON.prv
    }

    #$output
    $resultObject
}


function Format-ApiPath {
    <#
    .Synopsis
        Format-ApiPath
 
    .DESCRIPTION
        Ensure the right format of api path uri
 
    .PARAMETER Path
        Path to format
 
    .PARAMETER QueryParameter
        A hashtable for all the parameters to the api route
 
    .PARAMETER Token
        AccessToken object for Personio service
 
    .EXAMPLE
        Format-ApiPath -Path $ApiPath
 
        Api path data from variable $ApiPath will be tested and formatted.
 
    .NOTES
        Author: Andreas Bellstedt
 
    .LINK
        https://github.com/AndiBellstedt/PSPersonio
    #>

    [CmdletBinding(
        SupportsShouldProcess = $false,
        ConfirmImpact = 'Low'
    )]
    Param(
        [Parameter(Mandatory = $true)]
        [string]
        $Path,

        [Personio.Core.AccessToken]
        $Token,

        [hashtable]
        $QueryParameter

    )

    if (-not $Token) { $Token = $script:PersonioToken }

    Write-PSFMessage -Level System -Message "Formatting API path '$($Path)'"

    # Remove no more need slashes
    $apiPath = $Path.Trim('/')

    # check on API path prefix
    if (-not $ApiPath.StartsWith($token.ApiUri)) {
        $apiPath = $token.ApiUri.Trim('/') + "/" + $apiPath
        Write-PSFMessage -Level System -Message "Add API prefix, finished formatting path to '$($apiPath)'"
    } else {
        Write-PSFMessage -Level System -Message "Prefix API path already present, finished formatting"
    }

    # If specified, process hashtable QueryParameters to valid parameters into uri
    if ($QueryParameter) {
        $apiPath = "$($apiPath)?"
        $i = 0
        foreach ($key in $QueryParameter.Keys) {
            if ($i -gt 0) {
                $apiPath = "$($apiPath)&"
            }

            if ("System.Array" -in ($QueryParameter[$Key]).psobject.TypeNames) {
                $parts = $QueryParameter[$Key] | ForEach-Object { "$($key)=$($_)" }
                $apiPath = "$($apiPath)$([string]::Join("&", $parts))"
            } else {
                $apiPath = "$($apiPath)$($key)=$($QueryParameter[$Key])"
            }

            $i++
        }
    }

    # Output Result
    $apiPath
}


function Get-AccessToken {
    <#
    .SYNOPSIS
        Get access token
 
    .DESCRIPTION
        Get currently registered access token
 
    .EXAMPLE
        PS C:\> Get-AccessToken
 
        Get currently registered access token
    #>

    [cmdletbinding(ConfirmImpact="Low")]
    param()

    Write-PSFMessage -Level System -Message "Retrieve token object Id '$($script:PersonioToken.TokenID)'" -Tag "AccessToken", "Get"
    $script:PersonioToken

}


function New-AccessToken {
    <#
    .SYNOPSIS
        Create access token
 
    .DESCRIPTION
        Create access token
 
    .PARAMETER RawToken
        The RawToken data from personio service
 
    .PARAMETER ClientId
        The "UserName" of the API Token from personio service is used as "ClientId" within the service
 
    .EXAMPLE
        PS C:\> New-AccessToken -RawToken $rawToken -ClientId $ClientId
 
        Creates a Personio.Core.AccessToken from variable $rawToken
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingConvertToSecureStringWithPlainText", "")]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [cmdletbinding(PositionalBinding = $true)]
    [OutputType([Personio.Core.AccessToken])]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $RawToken,

        [String]
        $ClientId
    )

    $_date = Get-Date

    # Convert token to data object
    if ($RawToken.Contains(".") -and $RawToken.StartsWith("eyJ")) {
        # When API service give a JWT Token object
        Write-PSFMessage -Level System -Message "Decode token data" -Tag "AccessToken", "Create"
        $tokenInfo = ConvertFrom-JWTtoken -Token $RawToken
    } else {
        # Starting on June 2023 personio decides to step away from JWT tokens and began to invent a not parseable, service specific format
        $tokenInfo = [PSCustomObject]@{
            Id                   = (New-Guid)
            ClientId             = $ClientId
            ApplicationId        = $applicationIdentifier
            ApplicationPartnerId = $partnerIdentifier
            Issuer               = "$(Get-PSFConfigValue -FullName 'PSPersonio.API.URI' -Fallback '')"
            Scope                = @("PAPI", "Personio.API.Service")
            Token                = ($RawToken | ConvertTo-SecureString -AsPlainText -Force)
            ApiUri               = "$(Get-PSFConfigValue -FullName 'PSPersonio.API.URI' -Fallback '')"
            IssuedUTC            = $_date
            NotBeforeUTC         = $_date
            ExpiresUTC           = $_date.AddHours(24)
        }
    }

    # Create output token
    Write-PSFMessage -Level System -Message "Creating Personio.Core.AccessToken object" -Tag "AccessToken", "Create"
    $token = [Personio.Core.AccessToken]@{
        TokenID              = $tokenInfo.Id
        ClientId             = $tokenInfo.ClientId
        ApplicationId        = $applicationIdentifier
        ApplicationPartnerId = $partnerIdentifier
        Issuer               = $tokenInfo.Issuer
        Scope                = $tokenInfo.Scope
        Token                = ($RawToken | ConvertTo-SecureString -AsPlainText -Force)
        ApiUri               = "$(Get-PSFConfigValue -FullName 'PSPersonio.API.URI' -Fallback $tokenInfo.Issuer)"
        TimeStampCreated     = $tokenInfo.IssuedUTC.ToLocalTime()
        TimeStampNotBefore   = $tokenInfo.NotBeforeUTC.ToLocalTime()
        TimeStampExpires     = $tokenInfo.ExpiresUTC.ToLocalTime()
        TimeStampModified    = $_date.ToLocalTime()
    }

    # Output object
    $token
}


function New-PS1XML {
    <#
    .SYNOPSIS
        Register access token
 
    .DESCRIPTION
        Register access token within the module
 
    .PARAMETER Path
        The filename of the ps1xml file to create
 
    .PARAMETER TypeName
        Name of the type to create format file
 
    .PARAMETER PropertyList
        Name list of properties to put in format file
 
    .PARAMETER View
        The view to create in the format file
 
    .PARAMETER Encoding
        File encoding
 
    .PARAMETER PassThru
        Outputs the token to the console, even when the register switch is set
 
    .PARAMETER WhatIf
        If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run.
 
    .PARAMETER Confirm
        If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.
 
    .EXAMPLE
        PS C:\> New-PS1XML -Path C:\MyObject.Format.ps1xml -TypeName MY.Object -PropertyList $PropertyList
 
        Create MyObject.Format.ps1xml in C:\ with TypeFormat on object My.Object with property names set in $PropertyList
    #>

    [cmdletbinding(
        PositionalBinding = $true,
        SupportsShouldProcess = $true,
        ConfirmImpact = 'Medium'
    )]
    param(
        [ValidateNotNullOrEmpty()]
        [Parameter(Mandatory = $true)]
        [string]
        $Path,

        [ValidateNotNullOrEmpty()]
        [Parameter(Mandatory = $true)]
        [string]
        $TypeName,

        [ValidateNotNullOrEmpty()]
        [Parameter(Mandatory = $true)]
        [string[]]
        $PropertyList,

        [ValidateNotNullOrEmpty()]
        [ValidateSet("Table", "List", "Wide", "All")]
        [string[]]
        $View = "All",

        [ValidateNotNullOrEmpty()]
        [ValidateSet("UTF8", "UTF32", "UTF7", "Default", "Unicode", "ASCII", "BigEndianUnicode")]
        [string]
        $Encoding = "UTF8",

        [switch]
        $PassThru
    )

    # check Path
    Write-PSFMessage -Level Verbose -Message "Validate path: $($Path)" -Tag "FormatType", "Format.ps1xml"
    if (-not (Test-Path -Path $Path -IsValid -PathType Leaf)) {
        Stop-PSFFunction -Message "Path $($Path)) is not valid" -Tag "FormatType", "Format.ps1xml" -Cmdlet $pscmdlet
    }

    $tempPath = Join-Path -Path $env:TEMP -ChildPath "$((New-Guid).guid).format.ps1xml"
    Write-PSFMessage -Level System -Message "Start writing xml data in temporary file '$($tempPath)' ($($Encoding) encoding)" -Tag "FormatType", "Format.ps1xml", "tempfile"

    $XmlWriter = [System.XMl.XmlTextWriter]::new($tempPath, [System.Text.Encoding]::$Encoding)
    $xmlWriter.Formatting = "Indented"
    $xmlWriter.Indentation = "4"

    $xmlWriter.WriteStartDocument()

    #region <Configuration><ViewDefinitions>
    $xmlWriter.WriteStartElement("Configuration")
    $xmlWriter.WriteStartElement("ViewDefinitions")

    if ($View -like "Table" -or $View -like "All") {
        Write-PSFMessage -Level Verbose -Message "Generate table view for type $($TypeName)" -Tag "FormatType", "Format.ps1xml", "TableView"

        #region Start <View>
        $xmlWriter.WriteStartElement("View")

        # Element <Name>
        $xmlWriter.WriteElementString("Name", "Table_$($TypeName)")

        #region Start <ViewSelectedBy>
        $xmlWriter.WriteStartElement("ViewSelectedBy")
        $xmlWriter.WriteElementString("TypeName", "$($TypeName)")
        $xmlWriter.WriteEndElement()
        #endregion End <ViewSelectedBy>

        #region Start <TableControl>
        $xmlWriter.WriteStartElement("TableControl")

        # Element <AutoSize>
        $xmlWriter.WriteStartElement("AutoSize")
        $xmlWriter.WriteEndElement()

        #region Start <TableHeaders>
        $xmlWriter.WriteStartElement("TableHeaders")

        #region Start <TableColumnHeader>
        foreach ($property in $PropertyList) {
            $xmlWriter.WriteStartElement("TableColumnHeader")
            $xmlWriter.WriteElementString("Label", "$($property)")
            $xmlWriter.WriteEndElement()
        }
        #endregion End <TableColumnHeader>

        $xmlWriter.WriteEndElement()
        #endregion End <TableHeaders>

        #region Start <TableRowEntries><TableRowEntry><TableColumnItems>
        $xmlWriter.WriteStartElement("TableRowEntries")
        $xmlWriter.WriteStartElement("TableRowEntry")
        $xmlWriter.WriteStartElement("TableColumnItems")

        #region Start <TableColumnItem> <PropertyName>
        foreach ($property in $PropertyList) {
            $xmlWriter.WriteStartElement("TableColumnItem")
            $xmlWriter.WriteElementString("PropertyName", $property)
            $xmlWriter.WriteEndElement()
        }
        #endregion end <TableColumnItem> <PropertyName>

        $xmlWriter.WriteEndElement()
        $xmlWriter.WriteEndElement()
        $xmlWriter.WriteEndElement()
        #endregion End <TableRowEntries><TableRowEntry><TableColumnItems>

        $xmlWriter.WriteEndElement()
        #endregion End <TableControl>

        $xmlWriter.WriteEndElement()
        #endregion End <View>
    }

    if ($View -like "List" -or $View -like "All") {
        Write-PSFMessage -Level Verbose -Message "Generate list view for type $($TypeName)" -Tag "FormatType", "Format.ps1xml", "ListView"

        #region Start <View>
        $xmlWriter.WriteStartElement("View")

        # Element <Name>
        $xmlWriter.WriteElementString("Name", "List_$($TypeName)")

        #region Start <ViewSelectedBy>
        $xmlWriter.WriteStartElement("ViewSelectedBy")
        $xmlWriter.WriteElementString("TypeName", "$($TypeName)")
        $xmlWriter.WriteEndElement()
        #endregion End <ViewSelectedBy>

        #region Start <ListControl><ListEntries><ListEntry><ListItems>
        $xmlWriter.WriteStartElement("ListControl")
        $xmlWriter.WriteStartElement("ListEntries")
        $xmlWriter.WriteStartElement("ListEntry")
        $xmlWriter.WriteStartElement("ListItems")

        #region Start <ListItem> <PropertyName>
        foreach ($property in $PropertyList) {
            $xmlWriter.WriteStartElement("ListItem")
            $xmlWriter.WriteElementString("PropertyName", $property)
            $xmlWriter.WriteEndElement()
        }
        #endregion End <ListItem> <PropertyName>

        $xmlWriter.WriteEndElement()
        $xmlWriter.WriteEndElement()
        $xmlWriter.WriteEndElement()
        $xmlWriter.WriteEndElement()
        #endregion End <ListControl><ListEntries><ListEntry><ListItems>

        $xmlWriter.WriteEndElement()
        #endregion End <View>
    }

    if ($View -like "Wide" -or $View -like "All") {
        Write-PSFMessage -Level Verbose -Message "Generate wide view for type $($TypeName)" -Tag "FormatType", "Format.ps1xml", "WideView"

        #region Start <View>
        $xmlWriter.WriteStartElement("View")

        # Element <Name>
        $xmlWriter.WriteElementString("Name", "Wide_$($TypeName)")

        #region Start <ViewSelectedBy>
        $xmlWriter.WriteStartElement("ViewSelectedBy")
        $xmlWriter.WriteElementString("TypeName", "$($TypeName)")
        $xmlWriter.WriteEndElement()
        #endregion End <ViewSelectedBy>

        #region Start <WideControl><WideEntries><WideEntry>
        $xmlWriter.WriteStartElement("WideControl")
        $xmlWriter.WriteElementString("AutoSize", "")
        $xmlWriter.WriteStartElement("WideEntries")
        $xmlWriter.WriteStartElement("WideEntry")

        #region Start <WideItem> <PropertyName>
        $wideProperty = ""
        $wideProperty = $PropertyList | Where-Object { $_ -like "*name*"} | Sort-Object | Select-Object -First 1
        if(-not $wideProperty) {$wideProperty = $PropertyList | Where-Object { $_ -like "Id"} | Sort-Object | Select-Object -First 1}
        if(-not $wideProperty) {$wideProperty = $PropertyList | Select-Object -First 1}

        $xmlWriter.WriteStartElement("WideItem")
        $xmlWriter.WriteElementString("PropertyName", $wideProperty)
        $xmlWriter.WriteEndElement()
        #endregion End <WideItem> <PropertyName>

        $xmlWriter.WriteEndElement()
        $xmlWriter.WriteEndElement()
        $xmlWriter.WriteEndElement()
        #endregion End <WideControl><WideEntries><WideEntry>

        $xmlWriter.WriteEndElement()
        #endregion End <View>
    }

    $xmlWriter.WriteEndElement()
    $xmlWriter.WriteEndElement()
    #endregion <Configuration><ViewDefinitions>

    # End the XML Document
    $xmlWriter.WriteEndDocument()

    # Finish The Document
    $xmlWriter.Finalize
    $xmlWriter.Flush()
    $xmlWriter.Close()

    # Write file
    if ($pscmdlet.ShouldProcess("TypeFormat file '$($Path)' for type [$($TypeName)] with properties '$([string]::Join(", ", $PropertyList))'", "New")) {
        Write-PSFMessage -Level Verbose -Message "New TypeFormat file '$($Path)' for type [$($TypeName)] with properties '$([string]::Join(", ", $PropertyList))'" -Tag "FormatType", "Format.ps1xml", "New"

        $output = Move-Item -Path $tempPath -Destination $Path -Force -Confirm:$false -PassThru

        if($PassThru) {
            $output | Get-Item
        }
    }

}


function Register-AccessToken {
    <#
    .SYNOPSIS
        Register access token
 
    .DESCRIPTION
        Register access token within the module
 
    .PARAMETER Token
        The Token object to register
 
    .PARAMETER WhatIf
        If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run.
 
    .PARAMETER Confirm
        If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.
 
    .EXAMPLE
        PS C:\> Register-AccessToken -Token $token
 
        Register the Personio.Core.AccessToken from variable $rawToken to module wide vaiable $PersonioToken
    #>

    [cmdletbinding(
        PositionalBinding=$true,
        SupportsShouldProcess = $true
    )]
    param(
        [Parameter(Mandatory = $true)]
        [Personio.Core.AccessToken]
        $Token
    )

    # check if $PersonioToken already has data
    if($PersonioToken.TokenID) {
        Write-PSFMessage -Level System -Message "Replacing existing token object with Id '$($PersonioToken.TokenID)' with new token '$($Token.TokenID)' (valid until $($Token.TimeStampExpires))" -Tag "AccessToken", "Register"
    } else {
        Write-PSFMessage -Level System -Message "Register token '$($Token.TokenID)' (valid until $($Token.TimeStampExpires))" -Tag "AccessToken", "Register"
    }

    # register token
    if ($pscmdlet.ShouldProcess("AccessToken for ClientId '$($Token.ClientId)' from '$($Token.Issuer)' valid until $($Token.TimeStampExpires)", "Register")) {
        $script:PersonioToken = $Token
    }
}


function Get-PERSAbsence {
    <#
    .Synopsis
        Get-PERSAbsence
 
    .DESCRIPTION
        Retrieve absence periods from Personio tracked in days
        Parameters for filtered by period and/or specific employee(s) are available.
 
        The result can be paginated and.
 
    .PARAMETER InputObject
        AbsencePeriod to call again
 
    .PARAMETER StartDate
        First day of the period to be queried.
 
    .PARAMETER EndDate
        Last day of the period to be queried.
 
    .PARAMETER UpdatedFrom
        Query the periods that created or modified from the date UpdatedFrom.
 
    .PARAMETER UpdatedTo
        Query the periods that created or modified until the date UpdatedTo
 
    .PARAMETER EmployeeId
        A list of Personio employee ID's to filter the result.
        The result filters including only absences of provided employees
 
    .PARAMETER InclusiveFiltering
        If specified, datefiltering will change it's behaviour
        Absence records that begin or end before specified StartDate or after specified EndDate will be outputted
 
    .PARAMETER ResultSize
        How much records will be returned from the api.
        Default is 200.
 
        Use this parameter, when function throw information about pagination
 
    .PARAMETER Token
        AccessToken object for Personio service
 
    .EXAMPLE
        PS C:\> Get-PERSAbsence
 
        Get all available absence periods
        (api-side-pagination will kick in at 200)
 
    .NOTES
        Author: Andreas Bellstedt
 
    .LINK
        https://github.com/AndiBellstedt/PSPersonio
    #>

    [CmdletBinding(
        DefaultParameterSetName = "Default",
        SupportsShouldProcess = $false,
        PositionalBinding = $true,
        ConfirmImpact = 'Low'
    )]
    Param(
        [Parameter(ParameterSetName = "Default")]
        [datetime]
        $StartDate,

        [Parameter(ParameterSetName = "Default")]
        [datetime]
        $EndDate,

        [Parameter(ParameterSetName = "Default")]
        [datetime]
        $UpdatedFrom,

        [Parameter(ParameterSetName = "Default")]
        [datetime]
        $UpdatedTo,

        [Parameter(
            ParameterSetName = "Default",
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true
        )]
        [ValidateNotNullOrEmpty()]
        [int[]]
        $EmployeeId,

        [Parameter(ParameterSetName = "Default")]
        [ValidateNotNullOrEmpty()]
        [int]
        $ResultSize,

        [Parameter(
            ParameterSetName = "ByType",
            Mandatory = $true,
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true
        )]
        [Personio.Absence.AbsencePeriod[]]
        $InputObject,

        [switch]
        $InclusiveFiltering,

        [ValidateNotNullOrEmpty()]
        [Personio.Core.AccessToken]
        $Token
    )

    begin {
        # define query parameters
        $queryParameter = [ordered]@{}

        # fill query parameters
        if ($ResultSize) {
            $queryParameter.Add("limit", $ResultSize)
            $queryParameter.Add("offset", 0)
        }
        if ($StartDate) { $queryParameter.Add("start_date", (Get-Date -Date $StartDate -Format "yyyy-MM-dd")) }
        if ($EndDate) { $queryParameter.Add("end_date", (Get-Date -Date $EndDate -Format "yyyy-MM-dd")) }
        if ($UpdatedFrom) { $queryParameter.Add("updated_from", (Get-Date -Date $UpdatedFrom -Format "yyyy-MM-dd")) }
        if ($UpdatedTo) { $queryParameter.Add("updated_to", (Get-Date -Date $UpdatedTo -Format "yyyy-MM-dd")) }
    }

    process {
        if (-not $MyInvocation.BoundParameters['Token']) { $Token = Get-AccessToken }

        $parameterSetName = $pscmdlet.ParameterSetName
        Write-PSFMessage -Level Debug -Message "ParameterNameSet: $($parameterSetName)" -Tag "AbsensePeriod"

        # fill pipedin query parameters
        if ($EmployeeId) { $queryParameter.Add("employees[]", [array]$EmployeeId) }

        # Prepare query
        $invokeParam = @{
            "Type"    = "GET"
            "ApiPath" = "company/time-offs"
            "Token"   = $Token
        }
        if ($queryParameter) { $invokeParam.Add("QueryParameter", $queryParameter) }

        # Execute query
        $responseList = [System.Collections.ArrayList]@()
        if ($parameterSetName -like "Default") {
            Write-PSFMessage -Level Verbose -Message "Getting available absence periods" -Tag "AbsensePeriod", "Query"

            $response = Invoke-PERSRequest @invokeParam

            # Check response and add to responseList
            if ($response.success) {
                $null = $responseList.Add($response)
            } else {
                Write-PSFMessage -Level Warning -Message "Personio api reported no data" -Tag "AbsensePeriod", "Query"
            }
        } elseif ($parameterSetName -like "ByType") {
            foreach ($absencePeriod in $InputObject) {
                Write-PSFMessage -Level Verbose -Message "Getting absence period Id $($absencePeriod.Id)" -Tag "AbsensePeriod", "Query"

                $invokeParam.ApiPath = "company/time-offs/$($absencePeriod.Id)"
                $response = Invoke-PERSRequest @invokeParam

                # Check respeonse and add to responeList
                if ($response.success) {
                    $null = $responseList.Add($response)
                } else {
                    Write-PSFMessage -Level Warning -Message "Personio api reported no data on absence Id $($absencePeriod.Id)" -Tag "AbsensePeriod", "Query"
                }

                # remove token param for further api calls, due to the fact, that the passed in token, is no more valid after previous api all (api will use internal registered token)
                if ($InputObject.Count -gt 1) { $invokeParam.Remove("Token") }
            }
        }
        Remove-Variable -Name response -Force -WhatIf:$false -Confirm:$false -Verbose:$false -Debug:$false -ErrorAction Ignore -WarningAction Ignore -InformationAction Ignore

        foreach ($response in $responseList) {
            # Check pagination / result limitation
            if ($response.metadata) {
                Write-PSFMessage -Level Significant -Message "Pagination detected! Retrieved records: $([Array]($response.data).count) of $($response.metadata.total_elements) total records (api call hast limit of $($response.limit) records and started on record number $($response.offset))" -Tag "AbsensePeriod", "Query", "WebRequest", "Pagination"
            }

            # Process result
            $output = [System.Collections.ArrayList]@()
            foreach ($record in $response.data) {
                Write-PSFMessage -Level Debug -Message "Working on record Id $($record.attributes.id) startDate: $($record.attributes.start_date) - endDate: $($record.attributes.end_date)" -Tag "AbsensePeriod", "ObjectCreation"

                # Create object
                $result = [Personio.Absence.AbsencePeriod]@{
                    BaseObject = $record.attributes
                    Id         = $record.attributes.id
                }
                $result.psobject.TypeNames.Insert(1, "Personio.Absence.$($record.type)")

                $result.Employee = [Personio.Employee.BasicEmployee]@{
                    BaseObject = $record.attributes.employee.attributes
                    Id         = $record.attributes.employee.attributes.id.value
                    Name       = "$($record.attributes.employee.attributes.last_name.value), $($record.attributes.employee.attributes.first_name.value)"
                }
                $result.Employee.psobject.TypeNames.Insert(1, "Personio.Employee.$($record.attributes.employee.type)")

                $result.Type = [Personio.Absence.AbsenceType]@{
                    BaseObject = $record.attributes.time_off_type.attributes
                    Id         = $record.attributes.time_off_type.attributes.id
                    Name       = $record.attributes.time_off_type.attributes.name
                }
                $result.Type.psobject.TypeNames.Insert(1, "Personio.Absence.$($record.attributes.time_off_type.type)")

                # add objects to output array
                $null = $output.Add($result)
            }
            Write-PSFMessage -Level Verbose -Message "Retrieve $($output.Count) objects of type [Personio.Absence.AbsencePeriod]" -Tag "AbsensePeriod", "Result"

            # Filtering
            if (-not $MyInvocation.BoundParameters['InclusiveFiltering']) {
                if ($StartDate) { $output = $output | Where-Object StartDate -ge $StartDate }
                if ($EndDate) { $output = $output | Where-Object EndDate -le $EndDate }
                if ($UpdatedFrom) { $output = $output | Where-Object UpdatedAt -ge $UpdatedFrom }
                if ($UpdatedTo) { $output = $output | Where-Object UpdatedAt -le $UpdatedTo }
            }

            # output final results
            Write-PSFMessage -Level Verbose -Message "Output $($output.Count) objects" -Tag "AbsenseType", "Result", "Output"
            $output
        }

        # Cleanup variable
        Remove-Variable -Name Token -Force -WhatIf:$false -Confirm:$false -Verbose:$false -Debug:$false -ErrorAction Ignore -WarningAction Ignore -InformationAction Ignore
        $queryParameter.remove('employees[]')
    }

    end {
    }
}


function Get-PERSAbsenceSummary {
    <#
    .Synopsis
        Get-PERSAbsenceSummary
 
    .DESCRIPTION
        Retrieve absence summery for a specific employee from Personio
 
    .PARAMETER Employee
        The employee to get the summary for
 
    .PARAMETER EmployeeId
        Employee ID to get the summary for
 
    .PARAMETER Filter
        The name of the absence type to filter on
 
    .PARAMETER IncludeZeroValues
        If this is specified, all the absence types will be outputted.
        Be default, only absence summary records with a balance value greater than 0 are returned
 
    .PARAMETER Token
        AccessToken object for Personio service
 
    .EXAMPLE
        PS C:\> Get-PERSAbsenceSummary -EmployeeId 111
 
        Get absence summary of all types on employee with ID 111
 
    .EXAMPLE
        PS C:\> Get-PERSAbsenceSummary -Employee (Get-PERSEmployee -Email john.doe@company.com)
 
        Get absence summary of all types on employee John Doe
 
    .EXAMPLE
        PS C:\> Get-PERSEmployee -Email john.doe@company.com | Get-PERSAbsenceSummary -Type "Vacation"
 
        Get absence summary of type 'vacation' on employee John Doe
 
    .NOTES
        Author: Andreas Bellstedt
 
    .LINK
        https://github.com/AndiBellstedt/PSPersonio
    #>

    [CmdletBinding(
        DefaultParameterSetName = "ApiNative",
        SupportsShouldProcess = $false,
        PositionalBinding = $true,
        ConfirmImpact = 'Medium'
    )]
    Param(
        [Parameter(
            ParameterSetName = "UserFriendly",
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true,
            Mandatory = $true
        )]
        [Personio.Employee.BasicEmployee[]]
        $Employee,

        [Parameter(
            ParameterSetName = "ApiNative",
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true,
            Mandatory = $true
        )]
        [int[]]
        $EmployeeId,

        [Alias("Type", "AbsenceType")]
        [string[]]
        $Filter,

        [switch]
        $IncludeZeroValues,

        [Personio.Core.AccessToken]
        $Token
    )

    begin {
        if ($MyInvocation.BoundParameters['Token']) {
            $absenceTypes = Get-PERSAbsenceType -Token $Token
        } else {
            $absenceTypes = Get-PERSAbsenceType
        }
        $newTokenRequired = $true
    }

    process {
        # collect Employees from piped in IDs
        if ($MyInvocation.BoundParameters['EmployeeId']) {
            $Employee = Get-PERSEmployee -InputObject $EmployeeId
        }


        # Process employees and gather data
        $output = [System.Collections.ArrayList]@()
        foreach ($employeeItem in $Employee) {
            # Prepare token
            if (-not $MyInvocation.BoundParameters['Token'] -or $newTokenRequired) { $Token = Get-AccessToken }

            # Prepare query
            $invokeParam = @{
                "Type"    = "GET"
                "ApiPath" = "company/employees/$($employeeItem.id)/absences/balance"
                "Token"   = $Token
            }

            # Execute query
            Write-PSFMessage -Level Verbose -Message "Getting absence summary for '$($employeeItem)'" -Tag "AbsenceSummary", "Query"
            $response = Invoke-PERSRequest @invokeParam

            # Check respeonse
            if ($response.success) {
                # Process result
                foreach ($record in $response.data) {
                    Write-PSFMessage -Level Debug -Message "Working on record $($record.name) (ID: $($record.id)) for '$($employeeItem)'" -Tag "AbsenceSummary", "ObjectCreation"

                    # process if filter is not specified or filter applies on record
                    if ((-not $Filter) -or ($Filter | ForEach-Object { $record.name -like $_ })) {

                        # Create object
                        $result = [Personio.Absence.AbsenceSummaryRecord]@{
                            BaseObject  = $record
                            AbsenceType = ($absenceTypes | Where-Object Id -eq $record.id)
                            Employee    = $employeeItem
                            "Category"  = $record.category
                            "Balance"   = $record.balance
                        }
                        $result.psobject.TypeNames.Insert(1, "Personio.Absence.$($record.type)")

                        # add objects to output array
                        $null = $output.Add($result)
                    }
                }
            } else {
                Write-PSFMessage -Level Warning -Message "Personio api reported no data" -Tag "AbsenceSummary", "Query"
            }
        }
        Write-PSFMessage -Level System -Message "Retrieve $($output.Count) objects of type [Personio.Absence.AbsenceSummaryRecord]" -Tag "AbsenceSummary", "Result"

        if (-not $MyInvocation.BoundParameters['IncludeZeroValues']) {
            $output = $output | Where-Object Balance -gt 0
        }

        # output final results
        Write-PSFMessage -Level Verbose -Message "Output $($output.Count) objects" -Tag "AbsenceSummary", "Result", "Output"
        $output


        # Cleanup variable
        Remove-Variable -Name Token -Force -WhatIf:$false -Confirm:$false -Verbose:$false -Debug:$false -ErrorAction Ignore -WarningAction Ignore -InformationAction Ignore
    }

    end {
    }
}


function Get-PERSAbsenceType {
    <#
    .Synopsis
        Get-PERSAbsenceType
 
    .DESCRIPTION
        Retrieve absence types from Personio
 
    .PARAMETER Name
        Name filter for absence types
 
    .PARAMETER Id
        Id filter for absence types
 
    .PARAMETER ResultSize
        How much records will be returned from the api.
        Default is 200.
 
        Use this parameter, when function throw information about pagination
 
    .PARAMETER Token
        AccessToken object for Personio service
 
    .EXAMPLE
        PS C:\> Get-PERSAbsenceType
 
        Get all available absence types
 
    .EXAMPLE
        PS C:\> Get-PERSAbsenceType -Name "Krankheit*"
 
        Get all available absence types with name "Krankheit*"
 
    .EXAMPLE
        PS C:\> Get-PERSAbsenceType -Id 10
 
        Get absence types with id 10
 
    .EXAMPLE
        PS C:\> Get-PERSAbsenceType -Id 10, 11, 12 -Name "*Krankheit*"
 
        Get absence types with id 10, 11, 12 as long, as name matches *Krankheit*
 
    .NOTES
        Author: Andreas Bellstedt
 
    .LINK
        https://github.com/AndiBellstedt/PSPersonio
    #>

    [CmdletBinding(
        SupportsShouldProcess = $false,
        PositionalBinding = $true,
        ConfirmImpact = 'Low'
    )]
    Param(
        [string[]]
        $Name,

        [int[]]
        $Id,

        [int]
        $ResultSize,

        [Personio.Core.AccessToken]
        $Token
    )

    begin {
    }

    process {
    }

    end {
        if (-not $MyInvocation.BoundParameters['Token']) { $Token = Get-AccessToken }


        # Prepare query
        $invokeParam = @{
            "Type"    = "GET"
            "ApiPath" = "company/time-off-types"
            "Token"   = $Token
        }
        if ($ResultSize) {
            $invokeParam.Add(
                "QueryParameter", @{
                    "limit"  = $ResultSize
                    "offset" = 0
                }
            )

        }

        # Execute query
        Write-PSFMessage -Level Verbose -Message "Getting available absence types" -Tag "AbsenseType", "Query"
        $response = Invoke-PERSRequest @invokeParam


        # Check respeonse
        if ($response.success) {
            # Check pagination / result limitation
            if ($response.metadata) {
                Write-PSFMessage -Level Significant -Message "Pagination detected! Retrieved records: $([Array]($response.data).count) of $($response.metadata.total_elements) total records (api call hast limit of $($response.limit) records and started on record number $($response.offset))" -Tag "AbsenseType", "Query", "WebRequest", "Pagination"
            }


            # Process result
            $output = [System.Collections.ArrayList]@()
            foreach ($record in $response.data) {
                Write-PSFMessage -Level Debug -Message "Working on record $($record.attributes.name) (ID: $($record.attributes.id))" -Tag "AbsenseType", "ObjectCreation"

                # Create object
                $result = [Personio.Absence.AbsenceType]@{
                    BaseObject = $record.attributes
                    Id         = $record.attributes.id
                    Name       = $record.attributes.name
                }
                $result.psobject.TypeNames.Insert(1, "Personio.Absence.$($record.type)")

                # add objects to output array
                $null = $output.Add($result)
            }
            Write-PSFMessage -Level Verbose -Message "Retrieve $($output.Count) objects of type [Personio.Absence.AbsenceType]" -Tag "AbsenseType", "Result"


            # Filtering
            if ($Name -and $output) {
                Write-PSFMessage -Level Verbose -Message "Filter by Name: $([string]::Join(", ", $Name))" -Tag "AbsenseType", "Filtering", "NameFilter"

                $newOutput = [System.Collections.ArrayList]@()
                foreach ($item in $output) {
                    foreach ($filter in $Name) {
                        $filterResult = $item | Where-Object Name -like $filter
                        if ($filterResult) { $null = $newOutput.Add($filterResult) }
                    }
                }

                $Output = $newOutput
                Remove-Variable -Name newOutput, filter, filterResult, item -Force -WhatIf:$false -Confirm:$false -Verbose:$false -Debug:$false -ErrorAction Ignore -WarningAction Ignore -InformationAction Ignore
            }

            if ($Id -and $output) {
                Write-PSFMessage -Level Verbose -Message "Filter by Id: $([string]::Join(", ", $Id))" -Tag "AbsenseType", "Filtering", "IdFilter"
                $output = $output | Where-Object Id -in $Id
            }


            # output final results
            Write-PSFMessage -Level Verbose -Message "Output $($output.Count) objects" -Tag "AbsenseType", "Result", "Output"
            $output

        } else {
            Write-PSFMessage -Level Warning -Message "Personio api reported no data" -Tag "AbsenseType", "Query"
        }
    }
}


function New-PERSAbsence {
    <#
    .Synopsis
        New-PERSAbsence
 
    .DESCRIPTION
        Adds absence period (tracked in days) into Personio service
 
    .PARAMETER Employee
        The employee to create a absence for
 
    .PARAMETER EmployeeId
        Employee ID to create an absence
 
    .PARAMETER AbsenceType
        The Absence type to create
 
    .PARAMETER AbsenceTypeId
        The Absence type to create
 
    .PARAMETER StartDate
        First day of absence period
 
    .PARAMETER EndDate
        Last day of absence period
 
    .PARAMETER HalfDayStart
        Weather the start date is a half-day off
 
    .PARAMETER HalfDayEnd
        Weather the end date is a half-day off
 
    .PARAMETER Comment
        Optional comment for the absence
 
    .PARAMETER SkipApproval
        Optional, default value is true.
        If set to false, the approval status of the absence request will be "pending"
        if an approval rule is set for the absence type in Personio.
        The respective approval flow will be triggered.
 
    .PARAMETER Token
        AccessToken object for Personio service
 
    .PARAMETER WhatIf
        If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run.
 
    .PARAMETER Confirm
        If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.
 
    .EXAMPLE
        PS C:\> New-PERSAbsence -Employee (Get-PERSEmployee -Email john.doe@company.com) -Type (Get-PERSAbsenceType -Name "Vacation") -StartDate 01.01.2023 -EndDate 05.01.2023
 
        Create a new absence for "John Doe" of type "Urlaub" from 01.01.2023 until 05.01.2023
 
    .NOTES
        Author: Andreas Bellstedt
 
    .LINK
        https://github.com/AndiBellstedt/PSPersonio
    #>

    [CmdletBinding(
        DefaultParameterSetName = "ApiNative",
        SupportsShouldProcess = $true,
        PositionalBinding = $true,
        ConfirmImpact = 'Medium'
    )]
    [OutputType([Personio.Absence.AbsencePeriod])]
    Param(
        [Parameter(
            ParameterSetName = "UserFriendly",
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true,
            Mandatory = $true
        )]
        [Personio.Employee.BasicEmployee]
        $Employee,

        [Parameter(
            ParameterSetName = "ApiNative",
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true,
            Mandatory = $true
        )]
        [int]
        $EmployeeId,

        [Parameter(
            ParameterSetName = "UserFriendly",
            Mandatory = $true
        )]
        [Alias("Type", "Absence")]
        [Personio.Absence.AbsenceType]
        $AbsenceType,

        [Parameter(
            ParameterSetName = "ApiNative",
            Mandatory = $true
        )]
        [Alias("TypeId")]
        [int]
        $AbsenceTypeId,

        [Parameter(Mandatory = $true)]
        [datetime]
        $StartDate,

        [Parameter(Mandatory = $true)]
        [datetime]
        $EndDate,

        [bool]
        $HalfDayStart = $false,

        [bool]
        $HalfDayEnd = $false,

        [string]
        $Comment,

        [ValidateNotNullOrEmpty()]
        [bool]
        $SkipApproval = $true,

        [ValidateNotNullOrEmpty()]
        [Personio.Core.AccessToken]
        $Token
    )

    begin {
    }

    process {
        if (-not $MyInvocation.BoundParameters['Token']) { $Token = Get-AccessToken }
        $body = [ordered]@{}

        $parameterSetName = $pscmdlet.ParameterSetName
        Write-PSFMessage -Level Debug -Message "ParameterNameSet: $($parameterSetName)" -Tag "AbsensePeriod", "New"

        # fill pipedin query parameters
        if ($parameterSetName -like "ApiNative") {
            $body.Add("employee_id", $EmployeeId)
            $body.Add("time_off_type_id", $AbsenceTypeId)

        } elseif ($parameterSetName -like "UserFriendly") {
            $body.Add("employee_id", $Employee.Id)
            $body.Add("time_off_type_id", $AbsenceType.Id)

        }

        # fill query parameters
        $body.Add("start_date", (Get-Date -Date $StartDate -Format "yyyy-MM-dd"))
        $body.Add("end_date", (Get-Date -Date $EndDate -Format "yyyy-MM-dd"))
        $body.Add("half_day_start", $HalfDayStart.ToString().ToLower())
        $body.Add("half_day_end", $HalfDayEnd.ToString().ToLower())
        $body.Add("skip_approval", $SkipApproval.ToString().ToLower())
        #if ($MyInvocation.BoundParameters['Comment']) { $body.Add("comment", [uri]::EscapeDataString($Comment)) }
        if ($MyInvocation.BoundParameters['Comment']) { $body.Add("comment", $Comment) }

        # Debug logging
        foreach ($key in $body.Keys) {
            Write-PSFMessage -Level Debug -Message "Added body attribute '$($key)' with value '$($body[$key])'" -Tag "AbsensePeriod", "New", "Request"
        }

        # Prepare query
        $invokeParam = @{
            "Type"             = "POST"
            "ApiPath"          = "company/time-offs"
            "Token"            = $Token
            "Body"             = $body
            "AdditionalHeader" = @{
                "accept"       = "application/json"
                "content-type" = "application/x-www-form-urlencoded"
            }
        }

        $processMsg = "absence period of '$($AbsenceType.Name)' ($($body['start_date'])-$($body['end_date']))"
        if ($pscmdlet.ShouldProcess($processMsg, "New")) {
            Write-PSFMessage -Level Verbose -Message "New $($processMsg)" -Tag "AbsensePeriod", "New"

            # Execute query
            $response = Invoke-PERSRequest @invokeParam

            # Check response and add to responseList
            if ($response.success) {
                Write-PSFMessage -Level System -Message "Retrieve $(([array]$response.data).Count) objects" -Tag "AbsensePeriod", "New", "Result"

                foreach ($record in $response.data) {
                    # create absence object
                    $result = [Personio.Absence.AbsencePeriod]@{
                        BaseObject = $record.attributes
                        Id         = $record.attributes.id
                    }
                    $result.psobject.TypeNames.Insert(1, "Personio.Absence.$($record.type)")

                    # make employee record to valid object
                    $result.Employee = [Personio.Employee.BasicEmployee]@{
                        BaseObject = $record.attributes.employee.attributes
                        Id         = $record.attributes.employee.attributes.id.value
                        Name       = "$($record.attributes.employee.attributes.last_name.value), $($record.attributes.employee.attributes.first_name.value)"
                    }
                    $result.Employee.psobject.TypeNames.Insert(1, "Personio.Employee.$($record.attributes.employee.type)")

                    # make absenceType record to valid object
                    $result.Type = [Personio.Absence.AbsenceType]@{
                        BaseObject = $record.attributes.time_off_type.attributes
                        Id         = $record.attributes.time_off_type.attributes.id
                        Name       = $record.attributes.time_off_type.attributes.name
                    }
                    $result.Type.psobject.TypeNames.Insert(1, "Personio.Absence.$($record.attributes.time_off_type.type)")

                    # output final results
                    Write-PSFMessage -Level Verbose -Message "Output [$($result.psobject.TypeNames[0])] object '$($result.Type)' (start: $(Get-Date $result.StartDate -Format "yyyy-MM-dd") - end: $(Get-Date $result.EndDate -Format "yyyy-MM-dd"))" -Tag "AbsensePeriod", "Result", "Output"
                    $result
                }

            } else {
                Write-PSFMessage -Level Warning -Message "Personio api reported no data" -Tag "AbsensePeriod", "New"
            }
        }

        # Cleanup variable
        Remove-Variable -Name Token -Force -WhatIf:$false -Confirm:$false -Verbose:$false -Debug:$false -ErrorAction Ignore -WarningAction Ignore -InformationAction Ignore
        $body.remove('employee_id')
    }

    end {
    }
}


function Remove-PERSAbsence {
    <#
    .Synopsis
        Remove-PERSAbsence
 
    .DESCRIPTION
        Remove absence period (tracked in days) from Personio service
 
    .PARAMETER Absence
        The Absence to remove
 
    .PARAMETER AbsenceId
        The ID of the absence to remove
 
    .PARAMETER Force
        Suppress the user confirmation.
 
    .PARAMETER Token
        AccessToken object for Personio service
 
    .PARAMETER WhatIf
        If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run.
 
    .PARAMETER Confirm
        If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.
 
    .EXAMPLE
        PS C:\> $absence | Remove-PERSAbsence
 
        Remove absence from variable $absence. Assuming that $absence was previsouly filled with Get-PERSAbsence
 
    .EXAMPLE
        PS C:\> $absence | Remove-PERSAbsence -Force
 
        Remove absence from variable $absence silently. (Confirmation will be suppressed)
 
    .EXAMPLE
        PS C:\> Remove-PERSAbsence -AbsenceId 111
 
        Remove absence with ID 111
 
    .NOTES
        Author: Andreas Bellstedt
 
    .LINK
        https://github.com/AndiBellstedt/PSPersonio
    #>

    [CmdletBinding(
        DefaultParameterSetName = "ApiNative",
        SupportsShouldProcess = $true,
        PositionalBinding = $true,
        ConfirmImpact = 'High'
    )]
    Param(
        [Parameter(
            ParameterSetName = "UserFriendly",
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true,
            Mandatory = $true
        )]
        [Personio.Absence.AbsencePeriod]
        $Absence,

        [Parameter(
            ParameterSetName = "ApiNative",
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true,
            Mandatory = $true
        )]
        [int]
        $AbsenceId,

        [switch]
        $Force,

        [ValidateNotNullOrEmpty()]
        [Personio.Core.AccessToken]
        $Token
    )

    begin {
    }

    process {
        if (-not $MyInvocation.BoundParameters['Token']) { $Token = Get-AccessToken }

        $parameterSetName = $pscmdlet.ParameterSetName
        Write-PSFMessage -Level Debug -Message "ParameterNameSet: $($parameterSetName)" -Tag "AbsensePeriod", "Remove"

        # fill pipedin query parameters
        if ($parameterSetName -like "ApiNative") {
            $id = $AbsenceId
        } elseif ($parameterSetName -like "UserFriendly") {
            $id = $Absence.Id
        }

        # Prepare query
        $invokeParam = @{
            "Type"    = "DELETE"
            "ApiPath" = "company/time-offs/$($id)"
            "Token"   = $Token
        }

        $processMessage = "absence id '$($id)'"
        if($parameterSetName -like "UserFriendly") {
            $processMessage = $processMessage + " (" + $Absence.Type + " on '" + $Absence.Employee + "' for " + (Get-Date -Date $Absence.StartDate -Format "yyyy-MM-dd") + " - " + (Get-Date -Date $Absence.EndDate -Format "yyyy-MM-dd") + ")"
        }

        if(-not $Force) {
            if ($pscmdlet.ShouldProcess($processMessage, "Remove")) { $Force = $true }
        }

        if($Force) {
            Write-PSFMessage -Level Verbose -Message "Remove $($processMessage)" -Tag "AbsensePeriod", "Remove"

            # Execute query
            $response = Invoke-PERSRequest @invokeParam

            # Check response and add to responseList
            if ($response.success) {
                Write-PSFMessage -Level Verbose -Message "Absence id '$($id)' was removed. Message: $($response.data.message)" -Tag "AbsensePeriod", "Remove", "Result"
            } else {
                Write-PSFMessage -Level Warning -Message "Personio api reported no data" -Tag "AbsensePeriod", "Remove", "Result"
            }
        }

        # Cleanup variable
        Remove-Variable -Name Token,id, doRemove, processMessage -Force -WhatIf:$false -Confirm:$false -Verbose:$false -Debug:$false -ErrorAction Ignore -WarningAction Ignore -InformationAction Ignore
    }

    end {
    }
}


function Get-PERSAttendance {
    <#
    .Synopsis
        Get-PERSAttendance
 
    .DESCRIPTION
        Retrieve attendance data for the company employees
 
        Parameters for filtered by period and/or specific employee(s) are available
        The result can be paginated
 
    .PARAMETER StartDate
        First day to be queried
 
    .PARAMETER EndDate
        Last day to be queried
 
    .PARAMETER UpdatedFrom
        Query the periods that created or modified from the updated date
 
    .PARAMETER UpdatedTo
        Query the periods that created or modified until the updated date
 
    .PARAMETER EmployeeId
        A list of Personio employee ID's to filter the result.
        The result filters including only attendance of provided employees
 
    .PARAMETER IncludePending
        Returns attendance data with a status of pending, rejected and confirmed.
        For pending periods, the EndDate attribute is nullable.
 
        The status of each period is included in the response.
 
    .PARAMETER InclusiveFiltering
        If specified, datefiltering will change it's behaviour
        Attendance data records that begin or end before specified StartDate or after specified EndDate will be outputted
 
    .PARAMETER ResultSize
        How much records will be returned from the api
        Default is 200
 
        Use this parameter, when function throw information about pagination
 
    .PARAMETER Token
        AccessToken object for Personio service
 
    .EXAMPLE
        PS C:\> Get-PERSAttendance -StartDate 2023-01-01 -EndDate 2023-01-31
 
        Get attendance data from 2023-01-01 until 2023-01-31
        (api-side-pagination will kick in at 200)
 
    .NOTES
        Author: Andreas Bellstedt
 
    .LINK
        https://github.com/AndiBellstedt/PSPersonio
    #>

    [CmdletBinding(
        SupportsShouldProcess = $false,
        PositionalBinding = $true,
        ConfirmImpact = 'Low'
    )]
    Param(
        [Parameter(Mandatory = $true)]
        [datetime]
        $StartDate,

        [Parameter(Mandatory = $true)]
        [datetime]
        $EndDate,

        [ValidateNotNullOrEmpty()]
        [datetime]
        $UpdatedFrom,

        [ValidateNotNullOrEmpty()]
        [datetime]
        $UpdatedTo,

        [Parameter(
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true
        )]
        [ValidateNotNullOrEmpty()]
        [int[]]
        $EmployeeId,

        [ValidateNotNullOrEmpty()]
        [int]
        $ResultSize,

        [switch]
        $InclusiveFiltering,

        [ValidateNotNullOrEmpty()]
        [bool]
        $IncludePending = $true,

        [ValidateNotNullOrEmpty()]
        [Personio.Core.AccessToken]
        $Token
    )

    begin {
        # Cache for queried employees
        $listEmployees = [System.Collections.ArrayList]@()

        # define query parameters
        $_startDate = Get-Date -Date $StartDate -Format "yyyy-MM-dd"
        $_endDate = Get-Date -Date $EndDate -Format "yyyy-MM-dd"
        $queryParameter = [ordered]@{
            "start_date" = $_startDate
            "end_date"   = $_endDate
        }

        # fill query parameters
        if ($MyInvocation.BoundParameters['UpdatedFrom']) { $queryParameter.Add("updated_from", (Get-Date -Date $UpdatedFrom -Format "yyyy-MM-dd")) }
        if ($MyInvocation.BoundParameters['UpdatedTo']) { $queryParameter.Add("updated_to", (Get-Date -Date $UpdatedTo -Format "yyyy-MM-dd")) }
        if ($MyInvocation.BoundParameters['ResultSize']) {
            $queryParameter.Add("limit", $ResultSize)
            $queryParameter.Add("offset", 0)
        }
    }

    process {
        # basic preparation
        if (-not $MyInvocation.BoundParameters['Token']) { $Token = Get-AccessToken }

        $parameterSetName = $pscmdlet.ParameterSetName
        Write-PSFMessage -Level Debug -Message "ParameterNameSet: $($parameterSetName)" -Tag "Attendance"


        # fill pipedin query parameters
        if ($MyInvocation.BoundParameters['EmployeeId'] -and $EmployeeId) { $queryParameter.Add("employees[]", $EmployeeId) }


        # Prepare query
        $invokeParam = @{
            "Type"    = "GET"
            "ApiPath" = "company/attendances"
            "Token"   = $Token
        }
        if ($queryParameter) { $invokeParam.Add("QueryParameter", $queryParameter) }


        # Execute query
        Write-PSFMessage -Level Verbose -Message "Getting available attendance periods from $_startDate to $_endDate" -Tag "Attendance", "Query"

        $response = Invoke-PERSRequest @invokeParam


        # Check response and add to responseList
        if (-not $response.success) {
            Write-PSFMessage -Level Warning -Message "Personio api reported no data" -Tag "Attendance", "Query"
        }

        # Check pagination / result limitation
        if ($response.metadata.total_elements -gt $response.limit) {
            Write-PSFMessage -Level Significant -Message "Pagination detected! Retrieved records: $([Array]($response.data).count) of $($response.metadata.total_elements) total records (api call hast limit of $($response.limit) records and started on record number $($response.offset))" -Tag "Attendance", "Query", "WebRequest", "Pagination"
        }

        # Process result
        $output = [System.Collections.ArrayList]@()
        foreach ($record in $response.data) {
            Write-PSFMessage -Level Debug -Message "Working on record Id $($record.attributes.id) startDate: $($record.attributes.start_date) - endDate: $($record.attributes.end_date)" -Tag "Attendance", "ObjectCreation"

            # Create object
            $result = [Personio.Attendance.AttendanceRecord]@{
                BaseObject = $record.attributes
                Id         = $record.id
            }
            $result.psobject.TypeNames.Insert(1, "Personio.Absence.$($record.type)")

            # insert employee
            if ($listEmployees -and ($record.attributes.employee -in $listEmployees.Id)) {
                $_employee = $listEmployees | Where-Object Id -eq $record.attributes.employee
            } else {
                $_employee = Get-PERSEmployee -InputObject $record.attributes.employee | Select-Object -First 1
                $null = $listEmployees.Add($_employee)
            }
            $result.Employee = $_employee
            Remove-Variable -Name _employee -Force -WhatIf:$false -Confirm:$false -Verbose:$false -Debug:$false -ErrorAction Ignore -WarningAction Ignore -InformationAction Ignore

            #$result.Project = Get-PERSProject -InputObject $record.attributes.project

            # add objects to output array
            $null = $output.Add($result)
        }
        Write-PSFMessage -Level Verbose -Message "Retrieve $($output.Count) objects of type [Personio.Attendance.AttendanceRecord]" -Tag "Attendance", "Result"

        # Filtering
        if (-not $MyInvocation.BoundParameters['InclusiveFiltering']) {
            if ($StartDate) { $output = $output | Where-Object Date -ge $StartDate }
            if ($EndDate) { $output = $output | Where-Object Date -le $EndDate }
            if ($UpdatedFrom) { $output = $output | Where-Object UpdatedAt -ge $UpdatedFrom }
            if ($UpdatedTo) { $output = $output | Where-Object UpdatedAt -le $UpdatedTo }
        }

        # output final results
        Write-PSFMessage -Level Verbose -Message "Output $($output.Count) objects" -Tag "AbsenseType", "Result", "Output"
        $output

        # Cleanup variable
        Remove-Variable -Name Token -Force -WhatIf:$false -Confirm:$false -Verbose:$false -Debug:$false -ErrorAction Ignore -WarningAction Ignore -InformationAction Ignore
        $queryParameter.remove('employees[]')
    }

    end {
    }
}


function New-PERSAttendance {
    <#
    .Synopsis
        New-PERSAttendance
 
    .DESCRIPTION
        Add attendance records for the company employees into Personio service
 
    .PARAMETER Employee
        The employee to create a absence for
 
    .PARAMETER EmployeeId
        Employee ID to create an absence
 
    .PARAMETER Project
        The project to book on the attendance
 
    .PARAMETER ProjectId
        The id of the project to book on the attendance
 
    .PARAMETER Start
        Start of the attendance record as a datetime or parseable string value
        If only a time value is specified, the record will be today with the specified time.
 
        Attention, the date value of start and end has to be the same day!
 
    .PARAMETER End
        Start of the attendance record as a datetime or parseable string value
        If only a time value is specified, the record will be today with the specified time.
 
        Attention, the date value of start and end has to be the same day!
 
    .PARAMETER Break
        Minutes of break within the attendance record
 
    .PARAMETER Comment
        Optional comment for the attendance
 
    .PARAMETER SkipApproval
        Optional, default value is true.
        If set to false, the approval status of the attendance will be "pending"
        The respective approval flow will be triggered.
 
    .PARAMETER Token
        AccessToken object for Personio service
 
    .PARAMETER WhatIf
        If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run.
 
    .PARAMETER Confirm
        If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.
 
    .EXAMPLE
        PS C:\> New-PERSAttendance -Employee (Get-PERSEmployee -Email john.doe@company.com) -Start 08:00 -End 12:00
 
        Create a new attendance record for "John Doe" for "today" from 8 - 12am
 
    .NOTES
        Author: Andreas Bellstedt
 
    .LINK
        https://github.com/AndiBellstedt/PSPersonio
    #>

    [CmdletBinding(
        DefaultParameterSetName = "ApiNative",
        SupportsShouldProcess = $true,
        PositionalBinding = $true,
        ConfirmImpact = 'Medium'
    )]
    Param(
        [Parameter(
            ParameterSetName = "UserFriendly",
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true,
            Mandatory = $true
        )]
        [Personio.Employee.BasicEmployee[]]
        $Employee,

        [Parameter(
            ParameterSetName = "ApiNative",
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true,
            Mandatory = $true
        )]
        [int[]]
        $EmployeeId,

        [Parameter(
            ParameterSetName = "UserFriendly",
            Mandatory = $false
        )]
        [Personio.Project.ProjectRecord]
        $Project,

        [Parameter(
            ParameterSetName = "ApiNative",
            Mandatory = $false
        )]
        [int]
        $ProjectId,

        [Parameter(Mandatory = $true)]
        [datetime]
        $Start,

        [Parameter(Mandatory = $true)]
        [datetime]
        $End,

        [int]
        $Break = 0,

        [string]
        $Comment,

        [ValidateNotNullOrEmpty()]
        [bool]
        $SkipApproval = $true,

        [ValidateNotNullOrEmpty()]
        [Personio.Core.AccessToken]
        $Token
    )

    begin {
        $body = [ordered]@{
            "attendances"   = [System.Collections.ArrayList]@()
            "skip_approval" = [bool]$SkipApproval
        }

        $dateStart = Get-Date -Date $Start -Format "yyyy-MM-dd"
        $dateEnd = Get-Date -Date $End -Format "yyyy-MM-dd"
        if ($dateStart -ne $dateEnd) {
            Stop-PSFFunction -Message "Date problem, Start ($($dateStart)) and Stop ($($dateEnd)) parameters has different date values" -Tag "Attendance", "New", "StartEndDateDifference" -EnableException $true -Cmdlet $pscmdlet
        }
    }

    process {
        $parameterSetName = $pscmdlet.ParameterSetName
        Write-PSFMessage -Level Debug -Message "ParameterNameSet: $($parameterSetName)" -Tag "Attendance", "New"

        # fill piped in records
        if ($parameterSetName -like "UserFriendly") {
            $EmployeeId = $Employee.Id
            if ($MyInvocation.BoundParameters['Project']) { $ProjectId = $Project.Id } else { $ProjectId = 0 }
        }

        # work the pipe/ specified array of employees
        $attendances = [System.Collections.ArrayList]@()
        foreach ($employeeIdItem in $EmployeeId) {
            $attendance = [ordered]@{
                "employee"   = $employeeIdItem
                "date"       = $dateStart
                "start_time" = (Get-Date -Date $Start -Format "HH:mm")
                "end_time"   = (Get-Date -Date $End -Format "HH:mm")
                "break"      = [int]$Break
            }
            if ($ProjectId) { $attendance.Add("project_id", [int]$ProjectId) }
            if ($MyInvocation.BoundParameters['Comment']) { $attendance.Add("comment", $Comment) }

            # Debug logging
            Write-PSFMessage -Level Debug -Message "Added attendance: $($attendance | ConvertTo-Json -Compress)" -Tag "Attendance", "New", "Request", "Prepare"

            $null = $attendances.Add($attendance)
        }

        $null = $body['attendances'].Add( ($attendances | ForEach-Object { $_ }) )
    }

    end {
        # Prepare query
        if (-not $MyInvocation.BoundParameters['Token']) { $Token = Get-AccessToken }

        $invokeParam = @{
            "Type"             = "POST"
            "ApiPath"          = "company/attendances"
            "Token"            = $Token
            "Body"             = $body
            "AdditionalHeader" = @{
                "accept"       = "application/json"
                "content-type" = "application/json"
            }
        }

        $processMsg = "attendence for $(([array]$body.attendances.employee).count) employee(s)"
        if ($pscmdlet.ShouldProcess($processMsg, "New")) {
            Write-PSFMessage -Level Verbose -Message "New $($processMsg)" -Tag "Attendance", "New"

            # Execute query
            $response = Invoke-PERSRequest @invokeParam
            Remove-Variable -Name Token -Force -WhatIf:$false -Confirm:$false -Verbose:$false -Debug:$false -ErrorAction Ignore -WarningAction Ignore -InformationAction Ignore

            # Check response and add to responseList
            if ($response.success) {
                Write-PSFMessage -Level Verbose -Message "Attendance data created. API message: $($response.data.message)" -Tag "Attendance", "New", "Result"

                # Query attendance data created short before
                $_attendances = Get-PERSAttendance -StartDate $dateStart -EndDate (Get-Date -Date $Start.AddDays(1) -Format "yyyy-MM-dd") -UpdateFrom (Get-Date).AddMinutes(-5) -UpdateTo (Get-Date) -EmployeeId $body.attendances.employee

                # Output created attendance records
                $_attendances | Where-Object id -in $response.data.Id

            } else {
                Write-PSFMessage -Level Warning -Message "Personio api reported error: $($response.error)" -Tag "Attendance", "New"
            }
        }
    }
}


function Remove-PERSAttendance {
    <#
    .Synopsis
        Remove-PERSAttendance
 
    .DESCRIPTION
        Remove attendance data for the company employees from Personio service
 
    .PARAMETER Attendance
        The attendance to remove
 
    .PARAMETER AttendanceId
        The ID of the attendance to remove
 
    .PARAMETER SkipApproval
        Optional, default value is true.
        If set to false, the approval status within Personio service will be "pending"
        The respective approval flow will be triggered.
 
    .PARAMETER Force
        Suppress the user confirmation.
 
    .PARAMETER Token
        AccessToken object for Personio service
 
    .PARAMETER WhatIf
        If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run.
 
    .PARAMETER Confirm
        If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.
 
    .EXAMPLE
        PS C:\> $attendance | Remove-PERSAttendance
 
        Remove attendance records from variable $attendance. Assuming that $attendance was previsouly filled with Get-PERSAttendance
 
    .EXAMPLE
        PS C:\> $attendance | Remove-PERSAttendance -Force
 
        Remove attendance record from variable $attendance silently. (Confirmation will be suppressed)
 
    .EXAMPLE
        PS C:\> Remove-PERSAttendance -AttendanceId 111
 
        Remove attendance redord with ID 111
 
    .NOTES
        Author: Andreas Bellstedt
 
    .LINK
        https://github.com/AndiBellstedt/PSPersonio
    #>

    [CmdletBinding(
        DefaultParameterSetName = "ApiNative",
        SupportsShouldProcess = $true,
        PositionalBinding = $true,
        ConfirmImpact = 'High'
    )]
    Param(
        [Parameter(
            ParameterSetName = "UserFriendly",
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true,
            Mandatory = $true
        )]
        [Personio.Attendance.AttendanceRecord]
        $Attendance,

        [Parameter(
            ParameterSetName = "ApiNative",
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true,
            Mandatory = $true
        )]
        [int]
        $AttendanceId,

        [ValidateNotNullOrEmpty()]
        [bool]
        $SkipApproval = $true,

        [switch]
        $Force,

        [ValidateNotNullOrEmpty()]
        [Personio.Core.AccessToken]
        $Token
    )

    begin {
    }

    process {
        if (-not $MyInvocation.BoundParameters['Token']) { $Token = Get-AccessToken }

        $parameterSetName = $pscmdlet.ParameterSetName
        Write-PSFMessage -Level Debug -Message "ParameterNameSet: $($parameterSetName)" -Tag "Attendance", "Remove"

        # fill pipedin query parameters
        if ($parameterSetName -like "ApiNative") {
            $id = $attendanceId
        } elseif ($parameterSetName -like "UserFriendly") {
            $id = $attendance.Id
        }

        # Prepare query
        $invokeParam = @{
            "Type"           = "DELETE"
            "ApiPath"        = "company/attendances/$($id)"
            "Token"          = $Token
            "QueryParameter" = @{
                "skip_approval" = $SkipApproval.ToString().ToLower()
            }
            "AdditionalHeader" = @{
                "accept"       = "application/json"
            }
        }

        $processMessage = "attendance id '$($id)'"
        if ($parameterSetName -like "UserFriendly") {
            $processMessage = $processMessage + " (" + $attendance.Employee + " for "  + (Get-Date -Date $attendance.Start -Format "HH:mm") + " - " + (Get-Date -Date $attendance.End -Format "HH:mm") + " on " + (Get-Date -Date $attendance.Start -Format "yyyy-MM-dd") + ")"
        }

        if (-not $Force) {
            if ($pscmdlet.ShouldProcess($processMessage, "Remove")) { $Force = $true }
        }

        if ($Force) {
            Write-PSFMessage -Level Verbose -Message "Remove $($processMessage)" -Tag "Attendance", "Remove"

            # Execute query
            $response = Invoke-PERSRequest @invokeParam

            # Check response and add to responseList
            if ($response.success) {
                Write-PSFMessage -Level Verbose -Message "Attendance id '$($id)' was removed. Message: $($response.data.message)" -Tag "Attendance", "Remove", "Result"
            } else {
                Write-PSFMessage -Level Warning -Message "Personio api reported no data" -Tag "Attendance", "Remove", "Result"
            }
        }

        # Cleanup variable
        Remove-Variable -Name Token, id, doRemove, processMessage -Force -WhatIf:$false -Confirm:$false -Verbose:$false -Debug:$false -ErrorAction Ignore -WarningAction Ignore -InformationAction Ignore
    }

    end {
    }
}


function Connect-Personio {
    <#
    .Synopsis
        Connect-Personio
 
    .DESCRIPTION
        Connect to Personio Service
 
    .PARAMETER Credential
        The access token as a credential object to login
        This is the recommended way to use the function, due to security reason.
 
        Username has to be the Client_ID from api access manament of Personio
        Password has to be the Client_Secret from api access manament of Personio
 
    .PARAMETER ClientId
        The Client_ID from api access manament of Personio
 
        Even if prodived as a logon method, due to best practices and security reason, you should consider to use the Credential parameter to connect!
 
    .PARAMETER ClientSecret
        The Client_Secret from api access manament of Personio
 
        Even if prodived as a logon method, due to best practices and security reason, you should consider to use the Credential parameter to connect!
 
    .PARAMETER URL
        Name of the service to connect to.
        Default is 'https://api.personio.de' as predefined value, but you can -for whatever reason- change the uri if needed.
 
    .PARAMETER APIVersion
        Version of API endpoint to use
        Default is 'V1'
 
    .PARAMETER PassThru
        Outputs the token to the console
 
    .PARAMETER Confirm
        If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.
 
    .EXAMPLE
        PS C:\> Connect-Personio -Credential (Get-Credential "ClientID")
 
        Connects to "api.personio.de" with the specified credentials.
        Connection will be set as default connection for any further action.
 
    .NOTES
        Author: Andreas Bellstedt
 
    .LINK
        https://github.com/AndiBellstedt/PSPersonio
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingConvertToSecureStringWithPlainText", "")]
    [CmdletBinding(
        DefaultParameterSetName = 'Credential',
        SupportsShouldProcess = $false,
        PositionalBinding = $true,
        ConfirmImpact = 'Medium'
    )]
    Param(
        [Parameter(
            Mandatory = $true,
            ParameterSetName = 'Credential'
        )]
        [Alias("Token", "AccessToken", "APIToken")]
        [System.Management.Automation.PSCredential]
        $Credential,

        [Parameter(
            Mandatory = $true,
            ParameterSetName = 'PlainText'
        )]
        [Alias("Id")]
        [string]
        $ClientId,

        [Parameter(
            Mandatory = $true,
            ParameterSetName = 'PlainText'
        )]
        [Alias("Secret")]
        [string]
        $ClientSecret,

        [ValidateNotNullOrEmpty()]
        [Alias("ComputerName", "Hostname", "Host", "ServerName")]
        [uri]
        $URL = 'https://api.personio.de',

        [ValidateNotNullOrEmpty()]
        [Alias("Version")]
        [string]
        $APIVersion = "v1",

        [switch]
        $PassThru
    )

    begin {
    }

    process {
    }

    end {
        # Variable preperation
        [uri]$uri = $URL.AbsoluteUri + $APIVersion.Trim('/')

        [string]$applicationIdentifier = Get-PSFConfigValue -FullName 'PSPersonio.WebClient.ApplicationIdentifier' -Fallback "PSPersonio"
        [string]$partnerIdentifier = Get-PSFConfigValue -FullName 'PSPersonio.WebClient.PartnerIdentifier' -Fallback ""

        # Security checks
        if ($PsCmdlet.ParameterSetName -eq 'PlainText') {
            Write-PSFMessage -Level Warning -Message "You use potential unsecure login method! Even if prodived as a logon method, due to best practices and security reason, you should consider to use the Credential parameter to connect. Please take care about security and try to avoid plain text credentials." -Tag "Connection", "New", "Security", "PlainText"
        }

        # Extrect credential
        if ($PsCmdlet.ParameterSetName -eq 'Credential') {
            [string]$ClientId = $Credential.UserName
            [string]$ClientSecret = $Credential.GetNetworkCredential().Password
        }

        # Invoke authentication
        Write-PSFMessage -Level Verbose -Message "Authenticate '$($ClientId)' as application '$($applicationIdentifier)' to service '$($uri.AbsoluteUri)'" -Tag "Connection", "Authentication", "New"
        $body = @{
            "client_id"     = $($ClientId)
            "client_secret" = $($ClientSecret)
        }
        $paramRestMethod = @{
            "Uri"           = "$($uri.AbsoluteUri)/auth"
            "Headers"       = @{
                "X-Personio-Partner-ID" = $partnerIdentifier
                "X-Personio-App-ID"     = $applicationIdentifier
                "accept"                = "application/json"
                "content-type"          = "application/json"
            }
            "Body"          = ($body | ConvertTo-Json)
            "Method"        = "POST"
            "Verbose"       = $false
            "Debug"         = $false
            "ErrorAction"   = "Stop"
            "ErrorVariable" = "invokeError"
        }
        try {
            $response = Invoke-RestMethod @paramRestMethod -ContentType 'application/json'
        } catch {
            Stop-PSFFunction -Message "Error invoking rest call on service '$($uri.AbsoluteUri)'. $($invokeError)" -Tag "Connection", "Authentication", "New" -EnableException $true -Cmdlet $pscmdlet
        }

        # Check response
        if ($response.success -notlike "True") {
            Stop-PSFFunction -Message "Service '$($uri.AbsoluteUri)' processes the authentication request, but response does not succeed" -Tag "Connection", "Authentication", "New" -EnableException $true -Cmdlet $pscmdlet
        } elseif (-not $response.data.token) {
            Stop-PSFFunction -Message "Something went wrong on authenticating user '$($ClientId)'. No token found in authentication respeonse. Unable login to service '$($uri.AbsoluteUri)'" -Tag "Connection", "Authentication", "New" -EnableException $true -Cmdlet $pscmdlet
        } else {
            Set-PSFConfig -Module 'PSPersonio' -Name 'API.URI' -Value $uri.AbsoluteUri
        }

        # Create output token
        Write-PSFMessage -Level Verbose -Message "Set Personio.Core.AccessToken" -Tag "Connection", "AccessToken", "New"
        $token = New-AccessToken -RawToken $response.data.token -ClientId $ClientId

        # Register AccessToken for further commands
        Register-AccessToken -Token $token
        Write-PSFMessage -Level Significant -Message "Connected to service '($($token.ApiUri))' with ClientId '$($token.ClientId)'. TokenId: $($token.TokenID) valid for $($token.AccessTokenLifeTime.toString())" -Tag "Connection"

        # Output if passthru
        if ($PassThru) {
            Write-PSFMessage -Level Verbose -Message "Output Personio.Core.AccessToken to console" -Tag "Connection", "AccessToken", "New"
            $token
        }

        # Cleanup
        Clear-Variable -Name paramRestMethod, uri, applicationIdentifier, partnerIdentifier, ClientId, ClientSecret, Credential, response, token -Force -WhatIf:$false -Confirm:$false -Debug:$false -Verbose:$false -ErrorAction Ignore -WarningAction Ignore -InformationAction Ignore
    }
}


function Invoke-PERSRequest {
    <#
    .Synopsis
        Invoke-PersRequest
 
    .DESCRIPTION
        Basic function to invoke a API request to Personio service
 
        The function returns "raw data" from the API as a PSCustomObject.
        Titerally the function is a basic function within the core of the module.
        Most of the other functions, rely on Invoke-PresRequest to provide convenient data and functionality.
 
    .PARAMETER Type
        Type of web request
 
    .PARAMETER ApiPath
        Uri path for the REST call in the API
 
    .PARAMETER QueryParameter
        A hashtable for all the parameters to the api route
 
    .PARAMETER Body
        The body as a hashtable for the request
 
    .PARAMETER AdditionalHeader
        Additional headers to add in api call
        Provided as a hashtable
 
    .PARAMETER Token
        The TANSS.Connection token
 
    .PARAMETER WhatIf
        If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run.
 
    .PARAMETER Confirm
        If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.
 
    .EXAMPLE
        PS C:\> Invoke-PersRequest -Type GET -ApiPath "company/employees"
 
        Invoke a request to API route 'company/employees' as a GET call
 
    .NOTES
        Author: Andreas Bellstedt
 
    .LINK
        https://github.com/AndiBellstedt/PSTANSS
    #>

    [CmdletBinding(
        SupportsShouldProcess = $true,
        PositionalBinding = $true,
        ConfirmImpact = 'Medium'
    )]
    [OutputType([PSCustomObject])]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateSet("GET", "POST", "PUT", "DELETE")]
        [string]
        $Type,

        [Parameter(Mandatory = $true)]
        [string]
        $ApiPath,

        [hashtable]
        $QueryParameter,

        [hashtable]
        $Body,

        [hashtable]
        $AdditionalHeader,

        [Personio.Core.AccessToken]
        $Token
    )

    begin {
    }

    process {
    }

    end {
        #region Perpare variables
        # Check AccessToken
        if (-not $MyInvocation.BoundParameters['Token']) { $Token = Get-AccessToken }
        if (-not $Token) { Stop-PSFFunction -Message "No AccessToken found. Please connect to personio service frist. Use Connect-Personio command." -Tag "Connection", "MissingToken" -EnableException $true -Cmdlet $pscmdlet }
        if ($Token.IsValid) {
            Write-PSFMessage -Level System -Message "Valid AccessTokenId '$($Token.TokenID.ToString())' for service '$($Token.ApiUri)'." -Tag "WebRequest", "Token"
        } else {
            Stop-PSFFunction -Message "AccessTokenId '$($Token.TokenID.ToString())' is not valid. Please reconnect to personio service. Use Connect-Personio command." -Tag "Connection", "InvalidToken" -EnableException $true -Cmdlet $pscmdlet
        }


        # Get AppIds
        [string]$applicationIdentifier = Get-PSFConfigValue -FullName 'PSPersonio.WebClient.ApplicationIdentifier' -Fallback "PSPersonio"
        [string]$partnerIdentifier = Get-PSFConfigValue -FullName 'PSPersonio.WebClient.PartnerIdentifier' -Fallback ""


        # Format api path / api route to call
        $ApiPath = Format-ApiPath -Path $ApiPath -Token $Token -QueryParameter $QueryParameter


        # Format body
        if ($MyInvocation.BoundParameters['Body']) {
            $bodyData = $Body | ConvertTo-Json -Compress
            Write-PSFMessage -Level Debug -Message "BodyData: $($bodyData)" -Tag "WebRequest", "Body"
        } else {
            $bodyData = $null
        }


        # Format request header
        $header = @{
            "Authorization"         = "Bearer $([System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($Token.Token)))"
            "X-Personio-Partner-ID" = $partnerIdentifier
            "X-Personio-App-ID"     = $applicationIdentifier
        }

        if ($MyInvocation.BoundParameters['AdditionalHeader']) {
            foreach ($key in $AdditionalHeader.Keys) {
                $header.Add($key, $AdditionalHeader[$key])
            }
        }

        #endregion Perpare variables


        # Invoke the api request to the personio service
        $paramInvoke = @{
            "Uri"           = "$($ApiPath)"
            "Headers"       = $header
            "Body"          = $bodyData
            "Method"        = $Type
            "ContentType"   = 'application/json; charset=UTF-8'
            "Verbose"       = $false
            "Debug"         = $false
            "ErrorAction"   = "Stop"
            "ErrorVariable" = "invokeError"
        }

        if ($pscmdlet.ShouldProcess("$($Type) web REST call against URL '$($paramInvoke.Uri)'", "Invoke")) {
            Write-PSFMessage -Level Verbose -Message "Invoke $($Type) web REST call against URL '$($paramInvoke.Uri)'" -Tag "WebRequest", "Invoke"

            try {
                $response = Invoke-WebRequest @paramInvoke -UseBasicParsing
                $responseContent = $response.Content | ConvertFrom-Json
                Write-PSFMessage -Level System -Message "API Response: $($responseContent.success)"
            } catch {
                Write-PSFMessage -Level Error -Message "$($invokeError.Message) (StatusDescription:$($invokeError.ErrorRecord.Exception.Response.StatusDescription), Uri:$($ApiPath))" -Exception $invokeError.ErrorRecord.Exception -Tag "WebRequest", "Error", "API failure" -EnableException $true -PSCmdlet $pscmdlet
                return
            }

            # Create updated AccesToken from response. Every token can be used once and every api call will offer a new token
            Write-PSFMessage -Level System -Message "Update Personio.Core.AccessToken" -Tag "WebRequest", "Connection", "AccessToken", "Update"
            $token = New-AccessToken -RawToken $response.Headers['authorization'].Split(" ")[1]

            # Register updated AccessToken for further commands
            Register-AccessToken -Token $token
            Write-PSFMessage -Level Verbose -Message "Update AccessToken to Id '$($token.TokenID)'. Now valid up to $($token.TimeStampExpires.toString())" -Tag "WebRequest", "Connection", "AccessToken", "Update"

            # Check pagination
            if ($responseContent.metadata) {
                Write-PSFMessage -Level VeryVerbose -Message "Pagination detected! Retrieved records: $([Array]($responseContent.data).count) of $($responseContent.metadata.total_elements) total records (api call hast limit of $($responseContent.limit) records and started on record number $($responseContent.offset))" -Tag "WebRequest", "Pagination"
            }

            # Output data
            $responseContent
        }
    }
}

function Get-PERSEmployee {
    <#
    .Synopsis
        Get-PERSEmployee
 
    .DESCRIPTION
        List employee(s) from Personio
        The result can be paginated and.
 
    .PARAMETER InputObject
        Employee to call again
        It is inclusive, so the result starts from and including the provided StartDate
 
    .PARAMETER Email
        Find an employee with the given email address
 
    .PARAMETER UpdatedSince
        Find all employees that have been updated since the provided date
        NOTE: when using UpdatedSince, the Resultsize parameter is ignored
 
    .PARAMETER Attributes
        Define a list of whitelisted attributes that shall be returned for all employees
 
    .PARAMETER EmployeeId
        A list of Personio employee ID's to retrieve
 
    .PARAMETER ResultSize
        How much records will be returned from the api.
        Default is 200.
 
        Use this parameter, when function throw information about pagination
 
    .PARAMETER Token
        AccessToken object for Personio service
 
    .EXAMPLE
        PS C:\> Get-PERSEmployee
 
        Get all available company employees
        (api-side-pagination may kick in at 200)
 
    .NOTES
        Author: Andreas Bellstedt
 
    .LINK
        https://github.com/AndiBellstedt/PSPersonio
    #>

    [CmdletBinding(
        DefaultParameterSetName = "Default",
        SupportsShouldProcess = $false,
        PositionalBinding = $true,
        ConfirmImpact = 'Low'
    )]
    Param(
        [Parameter(
            ParameterSetName = "Default",
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true
        )]
        [string]
        $Email,

        [Parameter(ParameterSetName = "Default")]
        [datetime]
        $UpdatedSince,

        [Parameter(ParameterSetName = "Default")]
        [string[]]
        $Attributes,

        [Parameter(ParameterSetName = "Default")]
        [ValidateNotNullOrEmpty()]
        [int]
        $ResultSize,

        [Parameter(
            ParameterSetName = "ByType",
            Mandatory = $true,
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true
        )]
        [Alias("Id", "EmployeeId")]
        [Personio.Employee.BasicEmployee[]]
        $InputObject,

        [ValidateNotNullOrEmpty()]
        [Personio.Core.AccessToken]
        $Token
    )

    begin {
        # define script parameters
        $queryParameter = [ordered]@{}
        $typeNameBasic = "Personio.Employee.BasicEmployee"
        $typeNameExtended = "Personio.Employee.ExtendedEmployee"
        $memberNamesBasicEmployee = Expand-MemberNamesFromBasicObject -TypeName $typeNameBasic

        # fill query parameters
        if ($ResultSize) {
            $queryParameter.Add("limit", $ResultSize)
            $queryParameter.Add("offset", 0)
        }
        if ($UpdatedSince) { $queryParameter.Add("updated_since", (Get-Date -Date $UpdatedSince -Format "yyyy-MM-ddTHH:mm:ss")) }
        if ($attributes) { $queryParameter.Add("employees[]", $attributes) }
    }

    process {
        if (-not $MyInvocation.BoundParameters['Token']) { $Token = Get-AccessToken }
        $parameterSetName = $pscmdlet.ParameterSetName
        Write-PSFMessage -Level Debug -Message "ParameterNameSet: $($parameterSetName)" -Tag "Employee"

        # fill pipedin query parameters
        if ($Email) { $queryParameter.Add("email", $Email) }


        # Prepare query
        $invokeParam = @{
            "Type"    = "GET"
            "ApiPath" = "company/employees"
            "Token"   = $Token
        }
        if ($queryParameter) { $invokeParam.Add("QueryParameter", $queryParameter) }


        # Execute query
        $responseList = [System.Collections.ArrayList]@()
        if ($parameterSetName -like "Default") {
            Write-PSFMessage -Level Verbose -Message "Getting available employees" -Tag "Employee", "Query"

            $response = Invoke-PERSRequest @invokeParam

            # Check respeonse and add to responeList
            if ($response.success) {
                $null = $responseList.Add($response)
            } else {
                Write-PSFMessage -Level Warning -Message "Personio api reported no data" -Tag "Employee", "Query"
            }
        } elseif ($parameterSetName -like "ByType") {

            foreach ($inputItem in $InputObject) {
                Write-PSFMessage -Level Verbose -Message "Getting employee Id $($inputItem.Id)" -Tag "Employee", "Query"

                $invokeParam.ApiPath = "company/employees/$($inputItem.Id)"
                $response = Invoke-PERSRequest @invokeParam


                # Check respeonse and add to responeList
                if ($response.success) {
                    $null = $responseList.Add($response)
                } else {
                    Write-PSFMessage -Level Warning -Message "Personio api reported no data on employee Id $($inputItem.Id)" -Tag "Employee", "Query"
                }


                # remove token param for further api calls, due to the fact, that the passed in token, is no more valid after previous api all (api will use internal registered token)
                $invokeParam.Remove("Token")
            }
        }
        Remove-Variable -Name response -Force -WhatIf:$false -Confirm:$false -Verbose:$false -Debug:$false -ErrorAction Ignore -WarningAction Ignore -InformationAction Ignore


        foreach ($response in $responseList) {
            # Check pagination / result limitation
            if ($response.metadata) {
                Write-PSFMessage -Level Significant -Message "Pagination detected! Retrieved records: $([Array]($response.data).count) of $($response.metadata.total_elements) total records (api call hast limit of $($response.limit) records and started on record number $($response.offset))" -Tag "Employee", "Query", "WebRequest", "Pagination"
            }


            # Process result
            $output = [System.Collections.ArrayList]@()

            foreach ($record in $response.data) {
                Write-PSFMessage -Level Debug -Message "Working on record Id $($record.attributes.id.value) name: $($record.attributes.first_name.value) $($record.attributes.last_name.value)" -Tag "Employee", "ObjectCreation"

                # Create object
                $result = New-Object -TypeName $typeNameBasic -Property @{
                    BaseObject = $record.attributes
                    Id         = $record.attributes.id.value
                    Name       = "$($record.attributes.last_name.value), $($record.attributes.first_name.value)"
                }
                $result.psobject.TypeNames.Insert(1, "Personio.Employee.$($record.type)")

                #region dynamic attribute checking and typeData Format generation
                $dynamicAttributeNames = $result.BaseObject.psobject.Properties.name | Where-Object { $_ -ne "id" -and $_ -NotIn $memberNamesBasicEmployee }
                $dynamicAttributes = $result.BaseObject.psobject.Members | Where-Object name -in $dynamicAttributeNames
                if ($dynamicAttributes) {
                    # Dynamic attributes found
                    Write-PSFMessage -Level Debug -Message "Employee with dynamic attribute ('$([string]::Join("', '", $dynamicAttributeNames))') detected, create [$($typeNameExtended)] object" -Tag "Employee", "ObjectCreation", "ExtendedEmployee", "DynamicProperty"

                    # Add sythetic type on top of employee object
                    $result.psobject.TypeNames.Insert(0, $typeNameExtended)

                    # Check synthetic typeData definition & compile the gathered dynamic properties if needed
                    $typeExtendedEmployee = Get-TypeData -TypeName $typeNameExtended
                    $_modified = $false

                    foreach ($dynamicAttr in $dynamicAttributes) {
                        #$label = $dynamicAttr.Value.label.split(" ") | ConvertTo-CamelCaseString
                        # get property name
                        $memberName = $dynamicAttr.Value.label.split(" ") | ConvertTo-CamelCaseString
                        if (-not ($memberName -in $typeExtendedEmployee.Members.Keys)) {
                            # dynamic property is new and currently not in TypeData defintion

                            # check if property is a direct value or a PSObject
                            if (($dynamicAttr.Value.value.psobject.TypeNames | Select-Object -First 1) -like "System.Management.Automation.PSCustomObject") {
                                if ($dynamicAttr.Value.value.attributes.psobject.Properties.name -like "value") {
                                    [scriptblock]$value = [scriptblock]::Create( "`$this.BaseObject.$($dynamicAttr.Name).attributes.value" )
                                } elseif ($dynamicAttr.Value.value.attributes.psobject.Properties.name -like "name") {
                                    [scriptblock]$value = [scriptblock]::Create( "`$this.BaseObject.$($dynamicAttr.Name).attributes.name" )
                                }
                            } else {
                                [scriptblock]$value = [scriptblock]::Create( "`$this.BaseObject.$($dynamicAttr.Name).value" )
                            }

                            Write-PSFMessage -Level Debug -Message "Add dynamic attribute '$($dynamicAttr.Name)' as property '$($memberName)' into TypeData for [$($typeNameExtended)]" -Tag "Employee", "ObjectCreation", "ExtendedEmployee", "DynamicProperty", "TypeData"
                            Update-TypeData -TypeName $typeNameExtended -MemberType ScriptProperty -MemberName $memberName -Value $value -Force

                            $_modified = $true
                        }
                    }

                    if ($_modified -or (-not (Get-FormatData -TypeName $typeNameExtended))) {
                        Write-PSFMessage -Level Verbose -Message "New dynamic attributes within employee detected. TypeData for [Personio.Employee.ExtendedEmployee] was modified. Going to compile FormatData" -Tag "Employee", "ObjectCreation", "ExtendedEmployee", "DynamicProperty", "FormatData"
                        $typeBasicEmployee = Get-TypeData -TypeName $typeNameBasic
                        $pathExtended = Join-Path -Path $env:TEMP -ChildPath "$($typeNameExtended).Format.ps1xml"

                        $properties = @( "Id", "Name")
                        $properties += $typeBasicEmployee.Members.Keys
                        $properties += $dynamicAttributes.Value.label | ForEach-Object { $_.split(" ") | ConvertTo-CamelCaseString } #$typeExtendedEmployee.Members.Keys
                        $properties = $properties | Where-Object { $_ -notlike 'SerializationData' }

                        New-PS1XML -Path $pathExtended -TypeName $typeNameExtended -PropertyList $properties -View Table, List -Encoding UTF8

                        Write-PSFMessage -Level System -Message "Update FormatData with file '$($pathExtended)'" -Tag "Employee", "ObjectCreation", "ExtendedEmployee", "DynamicProperty", "FormatData"
                        Update-FormatData -PrependPath $pathExtended
                    }
                }
                #endregion dynamic attribute checking and typeData Format generation

                # add objects to output array
                $null = $output.Add($result)
            }

            if ($output.Count -gt 1) {
                Write-PSFMessage -Level Verbose -Message "Retrieve $(([string]::Join(" & ", ($output | ForEach-Object { $_.psobject.TypeNames[0] } | Group-Object | ForEach-Object { "$($_.count) [$($_.Name)]" })))) objects" -Tag "Employee", "Result"
            } else {
                Write-PSFMessage -Level Verbose -Message "Retrieve $($output.Count) object$(if($output) { " [" + $output[0].psobject.TypeNames[0] + "]"})" -Tag "Employee", "Result"
            }


            # Filtering
            #ToDo: Implement filtering for record output


            # output final results
            Write-PSFMessage -Level Verbose -Message "Output $($output.Count) objects" -Tag "Employee", "Result", "Output"
            foreach ($item in $output) {
                $item
            }
        }


        # Cleanup variable
        Remove-Variable -Name Token -Force -WhatIf:$false -Confirm:$false -Verbose:$false -Debug:$false -ErrorAction Ignore -WarningAction Ignore -InformationAction Ignore
        $queryParameter.remove('email')
    }

    end {
    }
}


<#
This is an example configuration file
 
By default, it is enough to have a single one of them,
however if you have enough configuration settings to justify having multiple copies of it,
feel totally free to split them into multiple files.
#>


<#
# Example Configuration
Set-PSFConfig -Module 'PSPersonio' -Name 'Example.Setting' -Value 10 -Initialize -Validation 'integer' -Handler { } -Description "Example configuration setting. Your module can then use the setting using 'Get-PSFConfigValue'"
#>


#region Module configurations
Set-PSFConfig -Module 'PSPersonio' -Name 'Import.DoDotSource' -Value $false -Initialize -Validation 'bool' -Description "Whether the module files should be dotsourced on import. By default, the files of this module are read as string value and invoked, which is faster but worse on debugging."
Set-PSFConfig -Module 'PSPersonio' -Name 'Import.IndividualFiles' -Value $false -Initialize -Validation 'bool' -Description "Whether the module files should be imported individually. During the module build, all module code is compiled into few files, which are imported instead by default. Loading the compiled versions is faster, using the individual files is easier for debugging and testing out adjustments."

Set-PSFConfig -Module 'PSPersonio' -Name 'WebClient.PartnerIdentifier' -Value "" -Initialize -Validation 'string' -Description "WebRequest header value - X-Personio-Partner-ID: The partner identifier"
Set-PSFConfig -Module 'PSPersonio' -Name 'WebClient.ApplicationIdentifier' -Value "PSPersonio" -Initialize -Validation 'string' -Description "WebRequest header value - X-Personio-App-ID: The application identifier that integrates with Personio"

Set-PSFConfig -Module 'PSPersonio' -Name 'API.URI' -Value "" -Initialize -Validation 'string' -Description "Base URI for API requests"
#endregion Module configurations



#region Module variables
New-Variable -Name PersonioToken -Scope Script -Visibility Public -Description "Variable for registered access token. This is for convinience use with the commands in the module" -Force

#endregion Module variables


<#
Stored scriptblocks are available in [PsfValidateScript()] attributes.
This makes it easier to centrally provide the same scriptblock multiple times,
without having to maintain it in separate locations.
 
It also prevents lengthy validation scriptblocks from making your parameter block
hard to read.
 
Set-PSFScriptblock -Name 'PSPersonio.ScriptBlockName' -Scriptblock {
     
}
#>


<#
# Example:
Register-PSFTeppScriptblock -Name "PSPersonio.alcohol" -ScriptBlock { 'Beer','Mead','Whiskey','Wine','Vodka','Rum (3y)', 'Rum (5y)', 'Rum (7y)' }
#>


<#
# Example:
Register-PSFTeppArgumentCompleter -Command Get-Alcohol -Parameter Type -Name PSPersonio.alcohol
#>


New-PSFLicense -Product 'PSPersonio' -Manufacturer 'Andreas.Bellstedt' -ProductVersion $script:ModuleVersion -ProductType Module -Name MIT -Version "1.0.0.0" -Date (Get-Date "2023-01-01") -Text @"
Copyright (c) 2023 Andreas.Bellstedt
 
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
 
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
 
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"@

#endregion Load compiled code