Sync.psm1

<#
.SYNOPSIS
    This function performs a sync of two lists of entities.
.DESCRIPTION
    This function performs a sync of two lists of entities.
    This is a generic sync framework which can be used to sync most types of entities,
    depending on the script blocks which are provided.
#>

function Sync-Entities {
    [CmdletBinding(PositionalBinding=$false)]
    param (
        # The entities which are currently present.
        [Parameter(Mandatory=$true)]
        [AllowEmptyCollection()]
        [Object[]]$current,

        # The entities which are expected to be present.
        [Parameter(Mandatory=$true)]
        [AllowEmptyCollection()]
        [Object[]]$expected,

        # The type of entity that is to be synced.
        # This identity is used in information, warning and error messages.
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [String]$entityType,

        # The property of the entity to use as the key.
        # Th value of this property must be not $null, and must be unique across all entities.
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [String]$entityKeyProperty,

        # The name of the entity property used as its identity.
        # This identity is used in information, warning and error messages, and can be
        # different from $entityKeyProperty.
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [String]$entityIdentityProperty,

        # Select the return type of this function.
        # This function can return the errors which occurred, which will be null if no errors occurred.
        # This function can also return the true/false result of whether the sync was successful.
        [Parameter(Mandatory=$false)]
        [ValidateSet("Errors", "Result")]
        [String]$returnType = "Result",

        # Select whether to create entities as part of the sync.
        [Parameter(Mandatory=$false)]
        [Boolean]$create = $false,

        # Select whether to update entities as part of the sync.
        [Parameter(Mandatory=$false)]
        [Boolean]$update = $false,

        # Select whether to delete/archive entities as part of the sync.
        [Parameter(Mandatory=$false)]
        [Boolean]$delete = $false,

        # Select whether the delete action deletes/archives entities.
        [Parameter(Mandatory=$false)]
        [ValidateSet("Archive", "Delete")]
        [String]$deleteAction = "Delete",

        # The script block used to un-archive an archived entity, if it exists.
        # Return values:
        # It should return "success" only if an archived entity was found as well as
        # successfully un-archived.
        # If no archived entity was found, return nothing.
        # Error handling:
        # This expression should throw an exception to indicate an error.
        [Parameter(Mandatory=$false)]
        [ValidateNotNull()]
        [ScriptBlock]$unArchiveEntity,

        # The script block used to create an entity.
        # Return values:
        # The script block should return "success" only if the entity was successfully created.
        # Error handling:
        # This expression should throw an exception to indicate an error.
        [Parameter(Mandatory=$true)]
        [ValidateNotNull()]
        [ScriptBlock]$createEntity,

        # The script block used to compare two entities for equality.
        # Return values:
        # $true if the two entities are equal, $false or $null otherwise.
        # Error handling:
        # None required.
        [Parameter(Mandatory=$true)]
        [ValidateNotNull()]
        [ScriptBlock]$compareEntities,

        # The expression used to update the properties of an entity.
        # Return values:
        # The script block should return "success" only if the entity was successfully updated.
        # Error handling:
        # This expression should throw an exception to indicate an error.
        [Parameter(Mandatory=$true)]
        [ValidateNotNull()]
        [ScriptBlock]$updateEntity,

        # The script block used to archive an entity.
        # Return values:
        # The script block should return "success" only if the entity was successfully archived.
        # Error handling:
        # This expression should throw an exception to indicate an error.
        [Parameter(Mandatory=$false)]
        [ValidateNotNull()]
        [ScriptBlock]$archiveEntity,

        # The script block used to delete an entity.
        # Return values:
        # The script block should return "success" only if the entity was successfully deleted.
        # Error handling:
        # This expression should throw an exception to indicate an error.
        [Parameter(Mandatory=$false)]
        [ValidateNotNull()]
        [ScriptBlock]$deleteEntity,

        # A list containing the objects which will be passed to the script blocks
        # as input parameters.
        [Parameter(Mandatory=$false)]
        [ValidateScript({ $_.length -gt 0 })]
        [Object[]]$parameterObjects,

        # A list of the names for the objects which will be passed to the script blocks
        # as input parameters.
        # The order of this list needs to correspond to the order of $parameterObjects.
        # These names do not need to be the same as the names of the objects passed to
        # $parameterObjects, as these are the names used exclusively in the
        # script blocks to refer to those objects.
        [Parameter(Mandatory=$false)]
        [ValidateScript({ $_.length -gt 0 -and ($_ | Where-Object { $_.GetType() -eq [String] }) -eq $_ })]
        [Object[]]$parameterNames
    )

    # This function adds additional lines to the beginning of a script block
    # in order for it to be able to accept input parameters from this function.
    function Add-ScriptBlockInputParameters {
        param (
            # The script block to update.
            [Parameter(Mandatory=$true)]
            [ValidateNotNull()]
            [ScriptBlock]$scriptBlock
        )
        # Generate the code for the script block parameters
        $scriptBlockParametersCode = "param("

        # Add the parameters
        if ($parameterNames) {
            foreach ($parameter in $parameterNames) {
                $scriptBlockParametersCode += "`$$($parameter),"
            }
        }

        # Add the entity as the last parameter
        $scriptBlockParametersCode += "`$entity)"

        # Add to script block and return
        Write-Verbose "Adding parameters $($scriptBlockParametersCode) to the script block"
        [ScriptBlock]::Create($scriptBlockParametersCode + $scriptBlock.ToString())
    }

    # Execute sync
    try {
        # Validate input parameters
        Write-Verbose "Validating `$parameterNames and `$parameterObjects."
        if ($parameterNames.length -ne $parameterObjects.length) {
            $errorMessages += "Length of `$parameterNames $($parameterNames.length) and `$parameterObjects $($parameterObjects.length) do not match."
            Write-Error $errorMessages
            throw $errorMessages
        }
        Write-Verbose "Validating parameter combinations for 'delete'."
        if ($delete) {
            if ($deleteAction -eq "Delete" -and [String]::IsNullOrWhiteSpace($deleteEntity)) {
                $errorMessages += "Delete is enabled and delete action is selected but expression to delete an entity has not been provided."
                Write-Error $errorMessages
                throw $errorMessages
            }
            elseif ($deleteAction -eq "Archive" -and [String]::IsNullOrWhiteSpace($archiveEntity)) {
                $errorMessages += "Delete is enabled and archive action is selected but expression to archive an entity has not been provided."
                Write-Error $errorMessages
                throw $errorMessages
            }
        }

        # Create hash table to help with syncing entities
        $entitiesHashTable = New-MergedObjectHashTable -CurrentObjects $current -ExpectedObjects $expected -KeyProperty $entityKeyProperty
        if ($null -eq $entitiesHashTable) {
            $errorMessages += "Failed to create merged hash table for syncing."
            Write-Error $errorMessages
            throw $errorMessages
        }

        # Create variable to store error messages
        $errorMessages = ""

        # Run through hash table to sync entities
        $index = 0
        foreach ($entity in $entitiesHashTable.Values) {
            # Provide updates on progress
            $index += 1
            $syncProgress = "$($entityType) $($index)/$($entitiesHashTable.Count)"

            # Entity is to be created/un-archived
            if ($create -and !$entity.Current -and $entity.Expected) {
                # Retrieve the entity identity
                $entityIdentity = $entity.Expected.$entityIdentityProperty
                Write-Information "$($syncProgress): Creating/un-archiving $($entityType) '$($entityIdentity)'."

                # Un-archive entity if script block is provided
                if ($unArchiveEntity) {
                    # Try to un-archive entity
                    try {
                        $scriptBlock = Add-ScriptBlockInputParameters $unArchiveEntity
                        $argumentList = $parameterObjects + @($entity)
                        $unArchiveResult = Invoke-Command -ScriptBlock $scriptBlock -ArgumentList $argumentList
                    }
                    catch {
                        $errorMessages += "$($syncProgress): Error while un-archiving $($entityType) '$($entityIdentity)'. `r`n$($_.Exception.Message)`r`n"
                        Write-Error $errorMessages
                        continue
                    }

                    # Un-archive was successful
                    if ($unArchiveResult -eq "success") {
                        Write-Verbose "$($syncProgress): Un-archived $($entityType) '$($entityIdentity)'."
                        continue
                    }
                }

                # Try to create entity
                try {
                    $scriptBlock = Add-ScriptBlockInputParameters $createEntity
                    $argumentList = $parameterObjects + @($entity)
                    $createResult = Invoke-Command -ScriptBlock $scriptBlock -ArgumentList $argumentList
                }
                catch {
                    $errorMessages += "$($syncProgress): Error while creating $($entityType) '$($entityIdentity)'. `r`n$($_.Exception.Message)`r`n"
                    Write-Error $errorMessages
                    continue
                }

                # Create was successful
                if ($createResult -eq "success") {
                    Write-Verbose "$($syncProgress): Created $($entityType) '$($entityIdentity)'."
                }
                else {
                    Write-Error "$($syncProgress): Failed to create $($entityType) '$($entityIdentity)'."
                }
            }

            # Entity is to be updated
            elseif ($update -and $entity.Current -and $entity.Expected) {
                # Retrieve the entity identity
                $entityIdentity = $entity.Current.$entityIdentityProperty

                # Only update entity if it is different from the expected entity
                $scriptBlock = Add-ScriptBlockInputParameters $compareEntities
                $argumentList = $parameterObjects + @($entity)
                $comparisonResult = Invoke-Command -ScriptBlock $scriptBlock -ArgumentList $argumentList
                if ($comparisonResult) {
                    Write-Information "$($syncProgress): $($entityType) '$($entityIdentity)' is up to date."
                    continue
                }

                # Continue with updating the entity
                Write-Information "$($syncProgress): Updating $($entityType) '$($entityIdentity)'."

                # Try to update the entity
                try {
                    $scriptBlock = Add-ScriptBlockInputParameters $updateEntity
                    $argumentList = $parameterObjects + @($entity)
                    $updateResult = Invoke-Command -ScriptBlock $scriptBlock -ArgumentList $argumentList
                }
                catch {
                    $errorMessages += "$($syncProgress): Error while updating $($entityType) '$($entityIdentity)'. `r`n$($_.Exception.Message)`r`n"
                    Write-Error $errorMessages
                    continue
                }

                # Update was successful
                if ($updateResult -eq "success") {
                    Write-Verbose "$($syncProgress): Updated $($entityType) '$($entityIdentity)'."
                }
                else {
                    Write-Error "$($syncProgress): Failed to update $($entityType) '$($entityIdentity)'."
                }
            }

            # Entity is to be archived/deleted
            elseif ($delete -and $entity.Current -and !$entity.Expected) {
                # Retrieve the entity identity
                $entityIdentity = $entity.Current.$entityIdentityProperty

                # Archive only if action is selected and archive expression is provided
                if ($deleteAction -eq "Archive" -and $archiveEntity) {
                    Write-Information "$($syncProgress): Archiving $($entityType) '$($entityIdentity)'."

                    # Try to archive the entity
                    try {
                        $scriptBlock = Add-ScriptBlockInputParameters $archiveEntity
                        $argumentList = $parameterObjects + @($entity)
                        $archiveResult = Invoke-Command -ScriptBlock $scriptBlock -ArgumentList $argumentList
                    }

                    # Error while archiving the entity
                    catch {
                        $errorMessages += "$($syncProgress): Error while archiving $($entityType) '$($entityIdentity)'. `r`n$($_.Exception.Message)`r`n"
                        Write-Error $errorMessages
                        continue
                    }

                    # Archive was successful
                    if ($archiveResult -eq "success") {
                        Write-Verbose "$($syncProgress): Archived $($entityType) '$($entityIdentity)'."
                    }
                    else {
                        Write-Error "$($syncProgress): Failed to archive $($entityType) '$($entityIdentity)'."
                    }
                }
                else {
                    Write-Information "$($syncProgress): Deleting $($entityType) '$($entityIdentity)'."

                    # Try to delete the entity
                    try {
                        $scriptBlock = Add-ScriptBlockInputParameters $deleteEntity
                        $argumentList = $parameterObjects + @($entity)
                        $deleteResult = Invoke-Command -ScriptBlock $scriptBlock -ArgumentList $argumentList
                    }

                    # Error while deleting the entity
                    catch {
                        $errorMessages += "$($syncProgress): Error while deleting $($entityType) '$($entityIdentity)'. `r`n$($_.Exception.Message)`r`n"
                        Write-Error $errorMessages
                        continue
                    }

                    # Delete was successful
                    if ($deleteResult -eq "success") {
                        Write-Verbose "$($syncProgress): Deleted $($entityType) '$($entityIdentity)'."
                    }
                    else {
                        Write-Error "$($syncProgress): Failed to delete $($entityType) '$($entityIdentity)'."
                    }
                }
            }

            # No action is to be taken
            else {
                # Retrieve the entity identity
                if ($entity.Expected) {
                    $entityIdentity = $entity.Expected.$entityIdentityProperty
                }
                else {
                    $entityIdentity = $entity.Current.$entityIdentityProperty
                }

                # Output the information message
                Write-Information "$($syncProgress): No action to be taken for $($entityType) '$($entityIdentity)'."
            }
        }
    }
    catch {
        # An exception means that the sync failed
        $errorMessages = "An exception occurred during this function on line $($_.InvocationInfo.ScriptLineNumber): $($_.Exception.Message)`r`n"
    }

    # Return the errors if there were any
    if (![String]::IsNullOrWhiteSpace($errorMessages)) {
        if ($returnType -eq "Errors") {
            return $errorMessages
        }
        # Return false to indicate unsuccessful sync
        else {
            return $false
        }
    }

    # Successful sync
    if ($returnType -eq "Result") {
        return $true
    }
}

<#
.SYNOPSIS
    This function returns the script blocks used to sync MSPComplete customers.
#>

function Get-SyncMSPCompleteCustomersScriptBlocks {
    # Return the script blocks
    return @{
        UnArchiveEntity = {
            # Retrieve the archived customer (if it exists)
            $archivedCustomer = Get-BT_Customer -Ticket $mspcObject.Ticket -CompanyName $entity.Expected.CompanyName `
                -WorkgroupId $mspcObject.Workgroup.Id -IsArchived $true -IsDeleted $false -Environment $environment

            # The archived customer doesn't exist
            if (!$archivedCustomer) {
                return
            }

            # If there are multiple archived customers, use the first one
            if ($archivedCustomer.length -gt 1) {
                $archivedCustomer = $archivedCustomer[0]
            }

            # Un-archive the customer
            $unArchivedCustomer = Set-BT_Customer -Ticket $mspcObject.Ticket -Customer $archivedCustomer `
                -IsArchived $false -Environment $environment
            if (!$unArchivedCustomer) {
                throw "Failed to un-archive previously archived customer."
            }
            "success"
        }
        CreateEntity = {
            # Create hash table for params
            $addBTCustomerParams = @{
                Ticket      = $mspcObject.Ticket
                Environment = $environment
            }

            # Add built-in properties to the params
            Get-MSPCompleteCustomerPropertyList | ForEach-Object -Process {
                if ($entity.Expected.$_) {
                    $addBTCustomerParams.Add($_, $entity.Expected.$_)
                }
            }

            # Create the customer
            $newCustomer = Add-BT_Customer @addBTCustomerParams
            if (!$newCustomer) {
                throw "Failed to create customer."
            }
            "success"
        }
        CompareEntities = {
            Compare-MSPCompleteCustomer -ReferenceCustomer $entity.Expected -ComparisonCustomer $entity.Current 2>$null
        }
        UpdateEntity = {
            # Create hash table for params
            $setBTCustomerParams = @{
                Ticket      = $mspcObject.Ticket
                Customer    = $entity.Current
                Environment = $environment
            }

            # Add built-in properties to the params
            Get-MSPCompleteCustomerPropertyList | ForEach-Object -Process {
                if ($entity.Expected.$_) {
                    $setBTCustomerParams.Add($_, $entity.Expected.$_)
                }
            }

            # Update the customer
            $updatedCustomer = Set-BT_Customer @setBTCustomerParams
            if (!$updatedCustomer) {
                throw "Failed to update customer."
            }
            "success"
        }
        ArchiveEntity = {
            # Archive the customer
            $archivedCustomer = Set-BT_Customer -Ticket $mspcObject.Ticket -Customer $entity.Current `
                -WorkgroupId $mspcObject.Workgroup.Id -IsArchived $true -Environment $environment
            if (!$archivedCustomer) {
                throw "Failed to archive customer."
            }
            "success"
        }
        DeleteEntity = {
            # Delete the customer
            Remove-BT_Customer -Ticket $mspcObject.Ticket -Id $entity.Current.Id `
                -Force -Environment $environment
            "success"
        }
    }
}

<#
.SYNOPSIS
    This function returns the script blocks used to sync MSPComplete users.
#>

function Get-SyncMSPCompleteUsersScriptBlocks {
    # Return the script blocks
    return @{
        UnArchiveEntity = {
            # Retrieve the archived user (if it exists)
            $archivedUser = Get-BT_CustomerEndUser -Ticket $mspcObject.CustomerTicket -DisplayName $entity.Expected.DisplayName `
                -IsArchived $true -IsDeleted $false -Environment $environment

            # The archived user doesn't exist
            if (!$archivedUser) {
                return
            }

            # If there are multiple archived users, use the first one
            if ($archivedUser.length -gt 1) {
                $archivedUser = $archivedUser[0]
            }

            # Un-archive the user
            $unArchivedUser = Set-BT_CustomerEndUser -Ticket $mspcObject.CustomerTicket -CustomerEndUser $archivedUser `
                                    -IsArchived $false -Environment $environment
            if (!$unArchivedUser) {
                throw "Failed to un-archive user."
            }
            "success"
        }
        CreateEntity = {
            # Create hash table for params
            $addBTCustomerEndUserParams = @{
                Ticket      = $mspcObject.CustomerTicket
                Environment = $environment
            }

            # Add built-in properties to the params
            Get-MSPCompleteUserPropertyList | ForEach-Object -Process {
                if ($entity.Expected.$_) {
                    $addBTCustomerEndUserParams.Add($_, $entity.Expected.$_)
                }
            }

            # Create the user
            $newUser = Add-BT_CustomerEndUser @addBTCustomerEndUserParams
            if (!$newUser) {
                throw "Failed to create user."
            }

            # Add extended properties to the user
            $extendedProperties = Get-MSPCompleteUserExtendedPropertyList
            foreach ($property in $extendedProperties) {
                if (![String]::IsNullOrWhiteSpace($entity.Expected.ExtendedProperties.$property)) {
                    $extendedProperty = Add-BT_ExtendedProperty -Ticket $mspcObject.WorkgroupTicket `
                        -ReferenceEntityId $newUser.Id -ReferenceEntityType "CustomerEndUser" -Name $property `
                        -Value $entity.Expected.ExtendedProperties.$property -Environment $environment
                    if (!$extendedProperty) {
                        throw "Failed to create extended property $($property):$($entity.Expected.ExtendedProperties.$property)."
                    }
                }
            }
            "success"
        }
        CompareEntities = {
            Compare-MSPCompleteUser -ReferenceUser $entity.Expected -ComparisonUser $entity.Current 2>$null
        }
        UpdateEntity = {
            # Create hash table for params
            $setBTCustomerEndUserParams = @{
                Ticket          = $mspcObject.CustomerTicket
                CustomerEndUser = $entity.Current
                Environment     = $environment
            }

            # Add built-in properties to the params
            Get-MSPCompleteUserPropertyList | ForEach-Object -Process {
                if ($entity.Expected.$_) {
                    $setBTCustomerEndUserParams.Add($_, $entity.Expected.$_)
                }
            }

            # Update the user
            $updatedUser = Set-BT_CustomerEndUser @setBTCustomerEndUserParams
            if (!$updatedUser) {
                throw "Failed to update user."
            }

            # Update the user extended properties
            $extendedProperties = Get-MSPCompleteUserExtendedPropertyList
            foreach ($property in $extendedProperties) {
                # Only update if different
                if ($entity.Current.ExtendedProperties.$property -ne $entity.Expected.ExtendedProperties.$property) {
                    $extendedProperty = Get-BT_ExtendedProperty -Ticket $mspcObject.WorkgroupTicket `
                        -ReferenceEntityId $updatedUser.Id -ReferenceEntityType "CustomerEndUser" `
                        -Name $property -Environment $environment

                    # Update existing extended property
                    if ($extendedProperty) {
                        $extendedProperty = Set-BT_ExtendedProperty -Ticket $mspcObject.WorkgroupTicket `
                            -ExtendedProperty $extendedProperty -Value $entity.Expected.ExtendedProperties.$property -Environment $environment
                    }

                    # Create new extended property
                    else {
                        $extendedProperty = Add-BT_ExtendedProperty -Ticket $mspcObject.WorkgroupTicket `
                            -ReferenceEntityId $updatedUser.Id -ReferenceEntityType "CustomerEndUser" `
                            -Name $property -Value $entity.Expected.ExtendedProperties.$property -Environment $environment
                    }

                    # Verify the extended property
                    if (!$extendedProperty) {
                        throw "Failed to update extended property $($property):$($entity.Expected.ExtendedProperties.$property)."
                    }
                }
            }
            "success"
        }
        ArchiveEntity = {
            # Archive the user
            $archivedUser = Set-BT_CustomerEndUser -Ticket $mspcObject.CustomerTicket -CustomerEndUser $entity.Current `
                                -IsArchived $true -Environment $environment
            if (!$archivedUser) {
                throw "Failed to archive user."
            }
            "success"
        }
        DeleteEntity = {
            # Delete the user
            Remove-BT_CustomerEndUser -Ticket $mspcObject.CustomerTicket -Id $entity.Current.Id `
                -Force -Environment $environment
            "success"
        }
    }
}