Public/Invoke-iPilotUserSync.ps1

Function Invoke-iPilotUserSync {
    <#
        .Description
        Syncs an Azure Active Directory group's users to iPilot

        .Parameter iPilotSyncGroupName
        The Azure Active Directory group name to sync

        .Parameter iPilotUsername
        The iPilot User Name to use for adding & removing users from iPilot

        .Parameter iPilotDataDirectory
        Location to store logs, delta URLs used for subsequent sync operations, and saved credentials

        .Parameter RetainLogsForDays
        Number (integer) of days to retain logs in iPilotDataDirectory

        .Parameter FilterByTelephoneNumber
        Filter the numbers received from iPilot by the digits specified (minimum 3 digits). Ex. the area code 775

        .Parameter ApiUrl
        iPilot API URL

        .Parameter Instance
        iPilot Synthesis Instance
        Instance is mandatory in iPilot module v1.1.0

        .Parameter TenantID
        Optional - If running Invoke-iPilotUserSync from a non-interactive session for the first time, provide the Azure Tenant ID:
            https://portal.azure.com/#view/Microsoft_AAD_IAM/TenantPropertiesBlade

        .Parameter ClientId
        Optional - If running Invoke-iPilotUserSync from a non-interactive session for the first time, provide the Azure Service Principal for syncing users

        .Parameter ClientSecret
        Optional - If running Invoke-iPilotUserSync from a non-interactive session for the first time, provide the Azure Service Principal Secret as a SecureString for syncing users

        .Parameter NoExit
        Optional - If running Invoke-iPilotUserSync from an Azure Automation Runbook, this can be used to run commands after the sync is complete

        .Example
        # Interactive on first run using "iPilot Assigned Phone Numbers" as the Azure AD group. Script will prompt for
            Tenant ID, Client ID, and Client Secret to be stored as an encrypted credential file
        Invoke-iPilotUserSync -iPilotUsername $iPilotUsername

        .Example
        # Non-interactive
        Invoke-iPilotUserSync `
            -iPilotSyncGroupName $iPilotSyncAzureADGroupName `
            -TenantID $TenantID `
            -ClientId $ClientId `
            -ClientSecret $SecureString `
            -iPilotUsername $iPilotUsername `
            -NoExit
    #>

    Param (
        [System.String]  [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()]
            $iPilotSyncGroupName = "iPilot Assigned Phone Numbers",
        [System.String] [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()]
            $iPilotUsername,
        [System.String] [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()]
            $iPilotDataDirectory = "${env:APPDATA}\iPilot",
        [System.Int16] [Parameter(Mandatory = $false)] [ValidateRange(1,[int]::MaxValue)]
            $RetainLogsForDays = 7,
        [System.Int16] [Parameter(Mandatory = $false)] [ValidatePattern('^[0-9]*$')]
            $FilterByTelephoneNumber,
        [System.String] [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()]
            $ApiUrl = "https://api.nuwave.com",
        [System.String] [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()]
            $Instance = $(throw "Instance is mandatory in iPilot module v1.1.0, please provide your iPilot Instance Name."),
        [System.String] [Parameter(Mandatory = $false)]
            $TenantID,
        [System.String] [Parameter(Mandatory = $false)]
            $ClientId,
        [SecureString] [Parameter(Mandatory = $false)]
            $ClientSecret,
        [Switch] [Parameter(Mandatory = $false)]
            $NoExit
    )

    # Verbose Switch
    if($PSBoundParameters.containskey("Verbose")) {
        $PreviousVerbosePreference = $VerbosePreference
        $VerbosePreference = "continue"
    }

    # Debug Switch
    if($PSBoundParameters.containskey("Debug")) {
        $PreviousDebugPreference = $DebugPreference
        $DebugPreference = "continue"
    }

    if ((Get-Module).Name -notcontains "iPilot") {
        Try {
            Import-Module iPilot -ErrorAction Stop
        } Catch {
            Install-Module iPilot -Force
            Import-Module iPilot
        }
    }

    # Create Data Directory
    New-Item -Path $iPilotDataDirectory -ItemType Directory -Force | Out-Null

    # Replace spaces in iPilotSyncGroupName for use in log & Delta file names
    $iPilotSyncGroupNameNoSpaces = $iPilotSyncGroupName -replace " ","_"
    Write-Verbose "iPilotSyncGroupNameNoSpaces:$($iPilotSyncGroupNameNoSpaces)"

    #region Start Logging

        $LogFile =  "$iPilotDataDirectory\$(Get-Date -Format FileDateTime)_$($iPilotSyncGroupNameNoSpaces)_Invoke-iPilotUserSync.log"
        Start-Transcript -Path $LogFile -Force

    #endregion Start Logging

    #region Parameter Validation

        # FilterByTelephoneNumber Validation
        if ($FilterByTelephoneNumber) {
            Write-Debug "`$FilterByTelephoneNumber.tostring().length:$($FilterByTelephoneNumber.tostring().length)"
            if($FilterByTelephoneNumber.tostring().length -lt 3 -or $FilterByTelephoneNumber.tostring().length -gt 15) {
                Write-Error "FilterByTelephoneNumber must be at least 4 digits and less than 15 digits. Exiting with failure."
                exit 1
            } else {
                Write-Output "Filtering numbers assigned to new users to numbers beginning with $FilterByTelephoneNumber"
            }
        } else {
            Write-Verbose "Not filtering by Telephone Number"
        }

    #endregion Parameter Validation

    #region Store/Retreive Credentials

        $InitializeiPilotSessionSplat = @{}

        # Add Instance to splat
        $InitializeiPilotSessionSplat += @{
            Instance = $Instance
        }

            # Add SaveToFile switch to splat
        if (Test-Path "$iPilotDataDirectory\iPilot.cred") {
            $InitializeiPilotSessionSplat += @{
                SaveToFile = $true
            }
        }

        Initialize-iPilotSession @InitializeiPilotSessionSplat -ApiUrl $ApiUrl
        
    #endregion Store/Retreive Credentials

    #region Authenticate to Azure & iPilot

        # Authenticate to iPilot
        Write-Verbose "Get iPilot Teams Domain for $global:IP_iPilotCredential"
        Get-iPilotTeamsDomain -Credential $global:IP_iPilotCredential

        #region Get Azure AD OAuth Application Token for Graph API

            # Store credential for unattended run
            Write-Debug "TenantID:$TenantID`nClientId:$ClientId`nClientSecret:$ClientSecret"
            if ($TenantID -and $ClientId -and $ClientSecret) {
                Write-Verbose "Tenant ID, ClientID, and Client Secret provided at runtime. Storing credentials in $iPilotDataDirectory\iPilotDirectorySyncCredential.cred"
                Write-Debug "`$global:IP_iPilotTenantID=$TenantID"
                $global:IP_iPilotTenantID = $TenantID
                
                Write-Debug "`$global:IP_iPilotClientID=$ClientId"
                $global:IP_iPilotClientID = $ClientId

                Write-Debug "`$global:IP_iPilotClientSecret=$ClientSecret"
                $global:IP_iPilotClientSecret = $ClientSecret

                Write-Debug "Creating credential with domain\username=$($global:IP_iPilotTenantID)\$($global:IP_iPilotClientID)"
                $global:IP_DirectorySyncCredential = New-Object System.Management.Automation.PSCredential ("$($global:IP_iPilotTenantID)\$($global:IP_iPilotClientID)", ($global:IP_iPilotClientSecret))
                $global:IP_DirectorySyncCredential | Export-Clixml -Path "$iPilotDataDirectory\iPilotDirectorySyncCredential.cred" -Force
            }
        
            # Get Token
            Get-iPilotDirectorySyncOAuthToken

            # Set Header
            $AzureADGraphHeaders = @{
                Authorization = "Bearer $global:IP_iPilotDirectorySyncOAuthToken"
            }
            Write-Verbose "Header: $($AzureADGraphHeaders | Out-String)"

        #endregion Get Azure AD OAuth Application Token for Graph API

    #endregion Authenticate to Azure & iPilot

    #region Get user and group changes from Azure AD

        #region Get Members of iPilot Sync group
            
            # Get Group's Object ID
            Try {
                $iPilotSyncGroupObjectIDUri = 'https://graph.microsoft.com/v1.0/groups?$filter=startswith(displayName,''' + [System.Web.HttpUtility]::UrlPathEncode($iPilotSyncGroupName) + ''')&$select=id'
                $iPilotSyncGroupObjectID = Invoke-RestMethod -Uri $iPilotSyncGroupObjectIDUri -Method Get -Headers $AzureADGraphHeaders | Select-Object -ExpandProperty value | Select-Object -ExpandProperty id
            } Catch {
                Write-Error "Failed to get Azure AD Group ID. Error:`n$($_.Exception | Format-List | Out-String)"
                Write-Debug "iPilotSyncGroupObjectIDUri=$iPilotSyncGroupObjectIDUri"
                Write-Debug "iPilotSyncGroupObjectID=$iPilotSyncGroupObjectID"
                exit 1
            }

            #region Get Sync Group Members

                $iPilotSyncGroupMembersUri = "https://graph.microsoft.com/v1.0/groups/$iPilotSyncGroupObjectId/members?`$select=id,userPrincipalName,displayName,givenName,surname,businessPhones,mobilePhone,accountEnabled"
                Try {
                    $iPilotSyncGroupMembersResponse = Invoke-RestMethod -Uri $iPilotSyncGroupMembersUri -Method Get -Headers $AzureADGraphHeaders
                    Write-Debug "Invoke-RestMethod -Method Get -Uri $iPilotSyncGroupMembersUri -Headers @(Authorization = ""Bearer $global:IP_iPilotDirectorySyncOAuthToken"") -ContentType ""application/json""`n"
                } Catch {
                    Write-Error "Failed to get members of $iPilotSyncGroupName"
                }
                $iPilotSyncGroupMembers = $iPilotSyncGroupMembersResponse | Select-Object -ExpandProperty value

                # Follow all nextLinks
                $i = 2
                Do {
                    # Get response from nextLink
                    if ($iPilotSyncGroupMembersResponse.'@odata.nextLink') {
                        Try {
                            $iPilotSyncGroupMembersResponse = Invoke-RestMethod -Method Get -Uri $iPilotSyncGroupMembersResponse.'@odata.nextLink' -Headers $AzureADGraphHeaders -ContentType "application/json"
                            Write-Debug "Invoke-RestMethod -Method Get -Uri $($iPilotSyncGroupMembersResponse.'@odata.nextLink') -Headers @(Authorization = ""Bearer $global:IP_iPilotDirectorySyncOAuthToken"") -ContentType ""application/json""`n"
                        } Catch {
                            Write-Error "Failed to pull initial list of all Azure AD Users"
                        }

                        $iPilotSyncGroupMembers += $iPilotSyncGroupMembersResponse | Select-Object -ExpandProperty value
                        Write-Verbose "`nGetDirectoryObjectsInitialRequest #$($i): $($iPilotSyncGroupMembersResponse.value.Count) records returned`n"
                        $i++
                    }
                }
                While ($iPilotSyncGroupMembersResponse.'@odata.nextLink' -and $iPilotSyncGroupMembersResponse.value.Count -ge 1)
                Write-Output "iPilotSyncGroupMembers:`n$($iPilotSyncGroupMembers | Format-Table | Out-String)`n"

            #endregion Get Sync Group Members

            #region Get Sync Group Member Telephone Numbers

                Foreach ($iPilotSyncGroupMember in $iPilotSyncGroupMembers) {
                    $iPilotSyncGroupMemberPropertiesUri = "https://graph.microsoft.com/v1.0/users/$($iPilotSyncGroupMember.id)?`$select=id,userPrincipalName,ipPhone,telephoneNumber,businessPhones,accountEnabled"
                    Write-Verbose "Appending ipPhone,telephoneNumber,businessPhones attributes to $($iPilotSyncGroupMember.userPrincipalName) object (id=$($iPilotSyncGroupMember.id))`nURI: $iPilotSyncGroupMemberPropertiesUri"
                    Try {
                        $iPilotSyncGroupMemberProperties = Invoke-RestMethod -Uri $iPilotSyncGroupMemberPropertiesUri -Method Get -Headers $AzureADGraphHeaders
                        Write-Debug "Invoke-RestMethod -Method Get -Uri $iPilotSyncGroupMemberPropertiesUri -Headers @(Authorization = ""Bearer $global:IP_iPilotDirectorySyncOAuthToken"") -ContentType ""application/json""`n"
                    } Catch {
                        Write-Warning "Failed to retrieve ipPhone,telephoneNumber,businessPhones attributes for $($iPilotSyncGroupMember.userPrincipalName)"
                    }

                    # Append Business Phone to object
                    if($iPilotSyncGroupMemberProperties.businessPhones) {
                        Write-Verbose "$($iPilotSyncGroupMemberProperties.userPrincipalName) - businessPhones = $($iPilotSyncGroupMemberProperties.businessPhones -join ",")"
                        Write-Verbose "$($iPilotSyncGroupMemberProperties.userPrincipalName) - businessPhone[0] (Digits Only) - $($iPilotSyncGroupMemberProperties.businessPhones[0] -replace "[^0-9]",'')"
                        $iPilotSyncGroupMember | Add-Member -MemberType NoteProperty -Name businessPhone -Value ($iPilotSyncGroupMemberProperties.businessPhones[0] -replace "[^0-9]",'') -Force

                    } else {
                        # Set business phone to $null if no businessPhones found
                        $iPilotSyncGroupMember | Add-Member -MemberType NoteProperty -Name businessPhone -Value $null
                        Write-Debug "$($iPilotSyncGroupMemberProperties.userPrincipalName) - businessPhone is empty"
                    }

                    # Append ipPhone to object
                    if ((Get-Member -InputObject $iPilotSyncGroupMemberProperties).Name -contains "ipPhone") {
                        Write-Verbose "$($iPilotSyncGroupMemberProperties.userPrincipalName) - ipPhone = $($iPilotSyncGroupMemberProperties.ipPhone)"
                        Write-Verbose "$($iPilotSyncGroupMemberProperties.userPrincipalName) - ipPhone (Digits Only) - $($iPilotSyncGroupMemberProperties.ipPhone -replace "[^0-9]",'')"
                        $iPilotSyncGroupMember | Add-Member -MemberType NoteProperty -Name ipPhone -Value ($iPilotSyncGroupMemberProperties.ipPhone -replace "[^0-9]","")
                    } else {
                        Write-Debug "$($iPilotSyncGroupMemberProperties.userPrincipalName) - ipPhone is empty"
                    }

                    # Append telephoneNumber to object
                    if ((Get-Member -InputObject $iPilotSyncGroupMemberProperties).Name -contains "telephoneNumber") {
                        Write-Verbose "$($iPilotSyncGroupMemberProperties.userPrincipalName) - telephoneNumber = $($iPilotSyncGroupMemberProperties.telephoneNumber)"
                        Write-Verbose "$($iPilotSyncGroupMemberProperties.userPrincipalName) - telephoneNumber (Digits Only) - $($iPilotSyncGroupMemberProperties.telephoneNumber -replace "[^0-9]",'')"
                        $iPilotSyncGroupMember | Add-Member -MemberType NoteProperty -Name telephoneNumber -Value ($iPilotSyncGroupMemberProperties.telephoneNumber -replace "[^0-9]","")
                    } else {
                        Write-Debug "$($iPilotSyncGroupMemberProperties.userPrincipalName) - telephoneNumber is empty"
                    }
                }

            #endregion Get Sync Group Member Telephone Numbers

        #endregion Get Members of iPilot Sync group

        #region Perform Initial Sync/Delta Sync & Get Changed Users
    
            $AzureADUsers = @()

            # Initial lookup of directory
            if (!(Test-Path -Path "$iPilotDataDirectory\UserDeltaLink_$($iPilotSyncGroupNameNoSpaces)_$($global:IP_iPilotDomain).txt") -and !$global:IP_iPilotUserDeltaLink) {

                Write-Output "Performing initial pull of all users from Azure AD`n"
            
                # Get initial lookup of id, displayName, userPrincipalName, givenName, surname, & accountEnabled
                $GetDirectoryObjectsInitialUri = 'https://graph.microsoft.com/v1.0/users/delta?$select=id,userPrincipalName,givenName,surname,accountEnabled,businessPhones'
                Write-Verbose "GetDirectoryObjectsInitialUri: $GetDirectoryObjectsInitialUri`n"
                Write-Verbose "=== GetDirectoryObjectsInitialUri #1: $($GetDirectoryObjectsInitialUri.Value.'members@delta'.id.Count) records returned ==="

                # Get initial user request response
                Try {
                    $GetDirectoryObjectsInitialRequest = Invoke-RestMethod -Method Get -Uri $GetDirectoryObjectsInitialUri -Headers $AzureADGraphHeaders -ContentType "application/json"
                    Write-Debug "Invoke-RestMethod -Method Get -Uri $GetDirectoryObjectsInitialUri -Headers @(Authorization = ""Bearer $global:IP_iPilotDirectorySyncOAuthToken"") -ContentType ""application/json""`n"
                } Catch {
                    Write-Error "Failed to pull initial list of all Azure AD Users. Error Message: $($_.Exception.Message)"
                }

                # Get initial values
                $AzureADUsers += $GetDirectoryObjectsInitialRequest.value

                # Follow all nextLinks
                $i = 2
                Do {
                    # Get response from nextLink
                    if ($GetDirectoryObjectsInitialRequest.'@odata.nextLink') {
                        Try {
                            $GetDirectoryObjectsInitialRequest = Invoke-RestMethod -Method Get -Uri $GetDirectoryObjectsInitialRequest.'@odata.nextLink' -Headers $AzureADGraphHeaders -ContentType "application/json"
                            Write-Debug "Invoke-RestMethod -Method Get -Uri $($GetDirectoryObjectsInitialRequest.'@odata.nextLink') -Headers @(Authorization = ""Bearer $global:IP_iPilotDirectorySyncOAuthToken"") -ContentType ""application/json""`n"
                        } Catch {
                            Write-Error "Failed to pull initial list of all Azure AD Users"
                        }

                        $AzureADUsers += $GetDirectoryObjectsInitialRequest.value
                        Write-Verbose "`nGetDirectoryObjectsInitialRequest #$($i): $($GetDirectoryObjectsInitialRequest.value.Count) records returned`n"
                        $i++
                    }
                }
                While ($GetDirectoryObjectsInitialRequest.'@odata.nextLink')
                Write-Verbose "All Azure AD users:`n$($AzureADUsers | Format-Table | Out-String)`n"
                Write-Debug "GetDirectoryObjectsInitialRequest looped $($i) times"

                # Output next link to deltaLink file for baseline
                if ($GetDirectoryObjectsInitialRequest.'@odata.deltaLink') {
                    Write-Output "Output next link to $("$iPilotDataDirectory\UserDeltaLink_$($iPilotSyncGroupNameNoSpaces)_$($global:IP_iPilotDomain).txt") for next sync`n"
                    $GetDirectoryObjectsInitialRequest.'@odata.deltaLink' | Out-File "$iPilotDataDirectory\UserDeltaLink_$($iPilotSyncGroupNameNoSpaces)_$($global:IP_iPilotDomain).txt" -Force
                    $global:IP_iPilotUserDeltaLink = $GetDirectoryObjectsInitialRequest.'@odata.deltaLink'
                    Write-Debug "`$GetDirectoryObjectsInitialRequest.'@odata.deltaLink'=$($GetDirectoryObjectsInitialRequest.'@odata.deltaLink')"
                } else {
                    Write-Error "No @odata.deltaLink returned from query to $GetDirectoryObjectsInitialUri"
                }

            } else {

                Write-Output "Performing delta pull of changed users from Azure AD`n"
            
                # Get DeltaLink URI from global variable or file
                if ($global:IP_iPilotUserDeltaLink) {
                    $GetDirectoryObjectsDeltaUri = $global:IP_iPilotUserDeltaLink
                } else {
                    $GetDirectoryObjectsDeltaUri = Get-Content -Path "$iPilotDataDirectory\UserDeltaLink_$($iPilotSyncGroupNameNoSpaces)_$($global:IP_iPilotDomain).txt"
                }

                # Get Delta
                Try {
                    $GetDirectoryObjectsDeltaRequest = Invoke-RestMethod -Method Get -Uri $GetDirectoryObjectsDeltaUri -Headers $AzureADGraphHeaders -ContentType "application/json"
                    Write-Debug "Invoke-RestMethod -Method Get -Uri $GetDirectoryObjectsDeltaUri -Headers @(Authorization = ""Bearer $global:IP_iPilotDirectorySyncOAuthToken"") -ContentType ""application/json""`n"
                } Catch {
                    Write-Error "Failed to pull delta list of all Azure AD Users. Error:`n$($_.Exception.Message)"
                }
                
                # Get Changed Users since last initial or delta request
                $AzureADUsers += $GetDirectoryObjectsDeltaRequest.value

                # Follow all nextLinks
                $i = 2
                Do {
                    # Get response from nextLink
                    if ($GetDirectoryObjectsDeltaRequest.'@odata.nextLink') {
                        Try {
                            $GetDirectoryObjectsDeltaRequest = Invoke-RestMethod -Method Get -Uri $GetDirectoryObjectsDeltaRequest.'@odata.nextLink' -Headers $AzureADGraphHeaders -ContentType "application/json"
                            Write-Debug "Invoke-RestMethod -Method Get -Uri $($GetDirectoryObjectsDeltaRequest.'@odata.nextLink') -Headers @(Authorization = ""Bearer $global:IP_iPilotDirectorySyncOAuthToken"") -ContentType ""application/json""`n"
                        } Catch {
                            Write-Error "Failed to pull delta list of all Azure AD Users. Error:`n$($_.Exception.Message))"
                        }

                        $AzureADUsers += $GetDirectoryObjectsDeltaRequest.value
                        Write-Verbose "GetDirectoryObjectsInitialRequest #$($i): $($GetDirectoryObjectsDeltaRequest.value.Count) records returned`n"
                        $i++
                    }
                }
                While ($GetDirectoryObjectsDeltaRequest.'@odata.nextLink')

                # Output next link to deltaLink file for next delta sync
                if ($GetDirectoryObjectsDeltaRequest.'@odata.deltaLink') {
                    Write-Output "Output next link to $("$iPilotDataDirectory\UserDeltaLink_$($iPilotSyncGroupNameNoSpaces)_$($global:IP_iPilotDomain).txt") for next sync`n"
                    $GetDirectoryObjectsDeltaRequest.'@odata.deltaLink' | Out-File "$iPilotDataDirectory\UserDeltaLink_$($iPilotSyncGroupNameNoSpaces)_$($global:IP_iPilotDomain).txt" -Force
                    $global:IP_iPilotUserDeltaLink = $GetDirectoryObjectsDeltaRequest.'@odata.deltaLink'
                    Write-Debug "`$GetDirectoryObjectsDeltaRequest.'@odata.deltaLink'=$($GetDirectoryObjectsDeltaRequest.'@odata.deltaLink')"
                } else {
                    Write-Error "No @odata.deltaLink returned from query to $GetDirectoryObjectsDeltaUri"
                }
            }
        
        #endregion Perform Initial Sync/Delta Sync & Get Changed Users

        #region Perform Initial Sync/Delta Lookup of iPilot Sync Group Members

            # Initial lookup of iPilot Sync Group Members
            if (!(Test-Path -Path "$iPilotDataDirectory\GroupDeltaLink_$($iPilotSyncGroupNameNoSpaces)_$($global:IP_iPilotDomain).txt") -and !$global:IP_iPilotGroupDeltaLink) {

                Write-Output "Performing initial query of $iPilotSyncGroupName members from Azure AD"
                
                # Get initial lookup of group members
                $GetDirectoryGroupInitialUri = "https://graph.microsoft.com/v1.0/groups/delta?`$filter=id eq `'" + $iPilotSyncGroupObjectID + "`'"
                Write-Verbose "GetDirectoryGroupsInitialUri: $GetDirectoryGroupsInitialUri"

                # Get initial group request response
                Try {
                    $GetDirectoryGroupInitialRequest = Invoke-RestMethod -Method Get -Uri $GetDirectoryGroupInitialUri -Headers $AzureADGraphHeaders -ContentType "application/json"
                    Write-Debug "`n`nInvoke-RestMethod -Method Get -Uri $GetDirectoryGroupInitialUri -Headers @(Authorization = ""Bearer $global:IP_iPilotDirectorySyncOAuthToken"") -ContentType ""application/json""`n`n"
                } Catch {
                    Write-Error "Failed to perform Initial Lookup of $iPilotSyncGroupName Members"
                }

                # Get initial values
                $iPilotSyncGroupMemberIds = $GetDirectoryGroupInitialRequest.Value.'members@delta'.id
                Write-Verbose "=== GetDirectoryGroupInitialRequest #1: $($GetDirectoryGroupInitialRequest.Value.'members@delta'.id.Count) records returned ==="

                # Follow all nextLinks
                $i = 2
                Do {
                    # Get response from nextLink
                    if ($GetDirectoryGroupInitialRequest.'@odata.nextLink') {
                        Try {
                            $GetDirectoryGroupInitialRequest = Invoke-RestMethod -Method Get -Uri $GetDirectoryGroupInitialRequest.'@odata.nextLink' -Headers $AzureADGraphHeaders -ContentType "application/json"
                            Write-Debug "`n`nInvoke-RestMethod -Method Get -Uri $($GetDirectoryGroupInitialRequest.'@odata.nextLink') -Headers @(Authorization = ""Bearer $global:IP_iPilotDirectorySyncOAuthToken"") -ContentType ""application/json""`n`n"
                        } Catch {
                            Write-Error "Failed to perform initial lookup of $iPilotSyncGroupName Members"
                        }

                        $iPilotSyncGroupMemberIds += $GetDirectoryGroupInitialRequest.Value.'members@delta'.id
                        Write-Verbose "=== GetDirectoryGroupInitialRequest #$($i): $($GetDirectoryGroupInitialRequest.Value.'members@delta'.id.Count) records returned ==="
                        $i++
                    }
                }
                While ($GetDirectoryGroupInitialRequest.'@odata.nextLink')
                Write-Verbose "iPilotSyncGroupMemberIds:`n$($iPilotSyncGroupMemberIds | Format-Table | Out-String)"
                Write-Debug "GetDirectoryGroupInitialRequest looped $($i) times"

                # Output deltaLink to file for next sync
                if ($GetDirectoryGroupInitialRequest.'@odata.deltaLink') {
                    Write-Output "Output next link to $("$iPilotDataDirectory\GroupDeltaLink_$($iPilotSyncGroupNameNoSpaces)_$($global:IP_iPilotDomain).txt") for next sync"
                    $GetDirectoryGroupInitialRequest.'@odata.deltaLink' | Out-File "$iPilotDataDirectory\GroupDeltaLink_$($iPilotSyncGroupNameNoSpaces)_$($global:IP_iPilotDomain).txt" -Force
                    $global:IP_iPilotGroupDeltaLink = $GetDirectoryGroupInitialRequest.'@odata.deltaLink'
                    Write-Debug "`$GetDirectoryGroupInitialRequest.'@odata.deltaLink'=$($GetDirectoryGroupInitialRequest.'@odata.deltaLink')"
                } else {
                    Write-Error "No @odata.deltaLink returned from query to $GetDirectoryGroupInitialUri"
                }

            } else {

                Write-Output "Performing delta query of group members of $iPilotSyncGroupName from Azure AD"
                
                # Get DeltaLink URI from global variable or file
                if ($global:IP_iPilotGroupDeltaLink) {
                    $GetDirectoryGroupDeltaUri = $global:IP_iPilotGroupDeltaLink
                } else {
                    $GetDirectoryGroupDeltaUri = Get-Content -Path "$iPilotDataDirectory\GroupDeltaLink_$($iPilotSyncGroupNameNoSpaces)_$($global:IP_iPilotDomain).txt"
                }

                # Get Delta
                Try {
                    $GetDirectoryGroupDeltaRequest = Invoke-RestMethod -Method Get -Uri $GetDirectoryGroupDeltaUri -Headers $AzureADGraphHeaders -ContentType "application/json"
                    Write-Debug "Invoke-RestMethod -Method Get -Uri $GetDirectoryGroupDeltaUri -Headers @(Authorization = ""Bearer $global:IP_iPilotDirectorySyncOAuthToken"") -ContentType ""application/json"""                    
                } Catch {
                    Write-Error "Failed to perform delta lookup of $iPilotSyncGroupName Members"
                }

                # Get delta values
                $iPilotSyncGroupMemberIds = $GetDirectoryGroupDeltaRequest.Value.'members@delta'.id
                Write-Verbose "Delta iPilotSyncGroupMemberIds:`n$($iPilotSyncGroupMemberIds | Format-Table | Out-String)"

                # Follow all nextLinks
                $i = 2
                Do {
                    # Get response from nextLink
                    if ($GetDirectoryGroupDeltaRequest.'@odata.nextLink') {
                        Try {
                            $GetDirectoryGroupDeltaRequest = Invoke-RestMethod -Method Get -Uri $GetDirectoryGroupDeltaRequest.'@odata.nextLink' -Headers $AzureADGraphHeaders -ContentType "application/json"
                            Write-Debug "Invoke-RestMethod -Method Get -Uri $($GetDirectoryGroupDeltaRequest.'@odata.nextLink') -Headers @(Authorization = ""Bearer $global:IP_iPilotDirectorySyncOAuthToken"") -ContentType ""application/json"""
                        } Catch {
                            Write-Error "Failed to perform delta lookup of $iPilotSyncGroupName Members"
                        }

                        $iPilotSyncGroupMemberIds += $GetDirectoryGroupDeltaRequest.Value.'members@delta'.id
                        Write-Verbose "GetDirectoryGroupDeltaRequest #$($i): $($GetDirectoryGroupDeltaRequest.Value.'members@delta'.id.Count) records returned"
                        $i++
                    }
                }
                While ($GetDirectoryGroupDeltaRequest.'@odata.nextLink')

                # Output iPilotSyncGroupMemberIds
                if ($iPilotSyncGroupMemberIds) {
                    Write-Debug "iPilotSyncGroupMemberIds:`n$($iPilotSyncGroupMemberIds | Format-Table | Out-String)"
                }

                # Get Users Added To iPilot Sync Group
                $UsersAddedToiPilotSyncGroup = $iPilotSyncGroupMemberIds | ForEach-Object {
                    $iPilotSyncGroupMemberId = $_
                    $iPilotSyncGroupMember = $iPilotSyncGroupMembers | Where-Object {$_.id -eq $iPilotSyncGroupMemberId}

                    # Set UsersAddedToiPilotSyncGroup to true
                    $iPilotSyncGroupMember | Add-Member -MemberType NoteProperty -Name AddedToiPilotSyncGroup -Value $true -Force
                    return $iPilotSyncGroupMember
                }

                # Output users added to iPilot Sync Group
                if ($UsersAddedToiPilotSyncGroup) {
                    Write-Output "Users Added To iPilot Sync Group:`n$($UsersAddedToiPilotSyncGroup | Select-Object givenName,surname,id,userPrincipalName,businessPhone,AddedToiPilotSyncGroup,accountEnabled,businessPhones | Format-Table -AutoSize | Out-String)"
                }

                # Append users to AzureAdUsers
                $AzureADUsers += $UsersAddedToiPilotSyncGroup

                #region Get Users Removed To iPilot Sync Group
                    $UsersRemovedFromiPilotSyncGroup = @()

                    if ($GetDirectoryGroupDeltaRequest.value.'members@delta') {

                        $GetDirectoryGroupDeltaRequest.value.'members@delta' | ForEach-Object {

                            Write-Verbose "GetDirectoryGroupDeltaRequest.value.'members@delta':`n$($_ | Format-Table | Out-String)"

                            if ($_.'@removed'.reason -eq "deleted") {
                        
                                $RemovediPilotSyncGroupMemberId = $_.id

                                #region Get user's details

                                    Write-Verbose "Getting user details for $RemovediPilotSyncGroupMemberId"
                                    $RemovediPilotSyncGroupMemberPropertiesUri = "https://graph.microsoft.com/v1.0/users/$($RemovediPilotSyncGroupMemberId)?`$select=id,userPrincipalName,givenName,surname,ipPhone,telephoneNumber,businessPhones,accountEnabled"
                                    Try {
                                        $RemovediPilotSyncGroupMemberProperties = Invoke-RestMethod -Uri $RemovediPilotSyncGroupMemberPropertiesUri -Method Get -Headers $AzureADGraphHeaders
                                        Write-Debug "Invoke-RestMethod -Method Get -Uri $RemovediPilotSyncGroupMemberPropertiesUri -Headers @(Authorization = ""Bearer $global:IP_iPilotDirectorySyncOAuthToken"") -ContentType ""application/json""`n"
                                    } Catch {
                                        Write-Warning "Failed to retrieve ipPhone,telephoneNumber,businessPhones attributes for Azure AD id: $RemovediPilotSyncGroupMemberId"
                                    }

                                #endregion Get user's details

                                $RemovediPilotSyncGroupMember = $RemovediPilotSyncGroupMemberProperties | Select-Object id,userPrincipalName,givenName,surname,businessPhones

                                Write-Verbose "Added $($RemovediPilotSyncGroupMember.userPrincipalName) to UsersRemovedToiPilotSyncGroup"
                                Write-Debug "Added $($RemovediPilotSyncGroupMember.userPrincipalName) to UsersRemovedToiPilotSyncGroup. Properties:`n$($RemovediPilotSyncGroupMember | Format-List | Out-String)"

                                # Set RemovedToiPilotSyncGroup to true
                                $RemovediPilotSyncGroupMember | Add-Member -MemberType NoteProperty -Name RemovedFromiPilotSyncGroup -Value $true -Force
                                $UsersRemovedFromiPilotSyncGroup += $RemovediPilotSyncGroupMember
                            }
                        }
                
                        # Append users to AzureAdUsers
                        $AzureADUsers += $UsersRemovedFromiPilotSyncGroup
                    }

                #endregion Get Users Removed To iPilot Sync Group

                # Output deltaLink to file for next sync
                if ($GetDirectoryGroupDeltaRequest.'@odata.deltaLink') {
                    Write-Output "Output next link to $("$iPilotDataDirectory\GroupDeltaLink_$($iPilotSyncGroupNameNoSpaces)_$($global:IP_iPilotDomain).txt") for next sync"
                    $GetDirectoryGroupDeltaRequest.'@odata.deltaLink' | Out-File "$iPilotDataDirectory\GroupDeltaLink_$($iPilotSyncGroupNameNoSpaces)_$($global:IP_iPilotDomain).txt" -Force
                    $global:IP_iPilotGroupDeltaLink = $GetDirectoryGroupDeltaRequest.'@odata.deltaLink'
                    Write-Debug "`$GetDirectoryGroupDeltaRequest.'@odata.deltaLink'=$($GetDirectoryGroupDeltaRequest.'@odata.deltaLink')"
                } else {
                    Write-Error "No @odata.deltaLink returned from query to $GetDirectoryGroupDeltaUri"
                }
            }
            
        #endregion Perform Initial Sync/Delta Lookup of iPilot Sync Group Members

        #region Get list of changed users

            # Output AzureADUsers
            if ($AzureADUsers) {
                Write-Output "Changed Azure AD users since last sync:`n$($AzureADUsers | Format-Table | Out-String)`n"
            }

            Write-Output "Getting changed users from last sync that are in $iPilotSyncGroupName"
            $ChangedUsers = @()
            $ChangedUsers += $AzureADUsers | Where-Object {
                    $ItemToCheck = $_
                    $iPilotSyncGroupMembers | Select-Object -ExcludeProperty AddedToiPilotSyncGroup | Where-Object { $ItemToCheck -match $_.id }
                }
            Write-Debug "ChangedUsers after UsersAddedToiPilotSyncGroup:`n$($ChangedUsers | Format-List | Out-String)"
                
            $ChangedUsers += $UsersAddedToiPilotSyncGroup
            Write-Debug "ChangedUsers after UsersAddedToiPilotSyncGroup:`n$($ChangedUsers | Format-Table | Out-String)"

            # Add UsersRemovedFromiPilotSyncGroup to ChangedUsers
            $ChangedUsers += $UsersRemovedFromiPilotSyncGroup
            Write-Debug "ChangedUsers after UsersRemovedFromiPilotSyncGroup:`n$($ChangedUsers | Format-Table | Out-String)"

            $ChangedUsers = $ChangedUsers | Where-Object {$_} | Select-Object -Unique * # Filters out any empty & duplicate users. Requires asterisk for bug with Select-Object -Unique: https://github.com/PowerShell/PowerShell/issues/17953
            Write-Debug "ChangedUsers after Select-Object -Unique *:`n$($ChangedUsers | Format-Table | Out-String)" -verbose

            Write-Verbose "iPilotSyncGroupMembers:`n$($iPilotSyncGroupMembers | Format-Table | Out-String)" -verbose

            if ($ChangedUsers) {
                Write-Output "Changed Users:`n$($ChangedUsers | Format-Table -AutoSize | Out-String)"
            }

        #endregion Get list of changed users

        #region Get objectGuid to iPilot ID mapping

            # Local JSON file path
            $JsonPath = "$iPilotDataDirectory\iPilotUserMapping_$($iPilotSyncGroupNameNoSpaces)_$($global:IP_iPilotDomain).json"

            # Import existing JSON if exists, else create JSON
            if (Test-Path $JsonPath) {
                Write-Verbose "Retrieving saved objectGUID and iPilot ID mapping located at $JsonPath"
                $SavediPilotUserMapping = Get-Content $JsonPath | ConvertFrom-Json
                if ($SavediPilotUserMapping) {
                    Write-Output "======== Saved User Mapping (SavediPilotUserMapping) ========:`n$($SavediPilotUserMapping | Format-Table -AutoSize | Out-String)`n"
                } else {
                    Write-Verbose "iPilot ID mapping located at $JsonPath is empty"
                }
            } else {
                $SavediPilotUserMapping = $global:IP_SavediPilotUserMapping
            }

            # Get all users from iPilot
            Try {
                Write-Output "Getting all assigned iPilot numbers "
                Get-iPilotTeamsDomain -Credential $iPilotCredential -ErrorAction Stop
                $AlliPilotUsers = Get-iPilotNumber -UserPrincipalNameAssigned | Where-Object {$_.UserPrincipalName}
            } Catch {
                Try {
                    Write-Output "Refreshing iPilot Token"
                    Get-iPilotTeamsDomain -Credential $iPilotCredential -RefreshToken -ErrorAction Stop
                    $AlliPilotUsers = Get-iPilotNumber -UserPrincipalNameAssigned | Where-Object {$_.UserPrincipalName}
                } Catch {
                    Write-Error "Failed to retreive all iPilot users. Exiting with failure.`nError: $($_.Exception.Message)"
                    if(!$NoExit.IsPresent) {
                        Stop-Transcript -Verbose
                        Exit 1
                    }
                }
            }
            Write-Debug "======= AlliPilotUsers: =======`n$($AlliPilotUsers | Format-List | Out-String)`n=======`n"
    
            if ($SavediPilotUserMapping) {
                Write-Output "Checking if any UPNs have changed from the SavediPilotUserMapping located at $JsonPath"
                $ChangedUserPrincipalNames = @()
                Foreach ($SavediPilotUser in $SavediPilotUserMapping) {

                    # Check if user is in saved JSON
                    $CurrentiPilotUser = $ChangedUsers | Where-Object {$_.id -eq $SavediPilotUser.objectGUID}
                    if ($CurrentiPilotUser) {
                        
                        # Check if UPN has changed
                        if ($CurrentiPilotUser.userPrincipalName -ne $SavediPilotUser.userPrincipalName) {
                            Write-Output "$($SavediPilotUser.userPrincipalName) has changed to $($CurrentiPilotUser.userPrincipalName)"
                            $CurrentiPilotUser  | Add-Member -MemberType NoteProperty -Name PreviousUPN -Value $SavediPilotUser.userPrincipalName -Force
                            $ChangedUserPrincipalNames += $CurrentiPilotUser
                            
                            # Update SavediPilotUserMapping with new UPN
                            $SavediPilotUserMappingToUpdate = $SavediPilotUserMapping | Where-Object {$_.userPrincipalName -eq $SavediPilotUser.userPrincipalName}
                            $SavediPilotUserMappingToUpdate.userPrincipalName = $CurrentiPilotUser.userPrincipalName
                        }
                    }
                }

                if ($ChangedUserPrincipalNames) {
                    Write-Output "`nThe below users have changed userPrincipalNames:`n$($ChangedUserPrincipalNames | Select-Object id,userPrincipalName,PreviousUPN | Format-Table | Out-String)`n"
                }
            }

            # Loop through each user found in Azure AD and map UserID, iPilotFirstName, and iPilotLastName
            Foreach ($User in $ChangedUsers) {

                # Add objectGUID to ChangedUsers
                $User | Add-Member -MemberType NoteProperty -Name objectGUID -Value $User.id -Force

                # Add UserID to ChangedUsers
                if ($User.PreviousUPN) {
                    $User | Add-Member -MemberType NoteProperty -Name UserID -Value (
                        $AlliPilotUsers | 
                            Where-Object {$_.UserPrincipalName -eq $User.PreviousUPN} | 
                                Select-Object -ExpandProperty UserID) -Force
                } else {
                    $User | Add-Member -MemberType NoteProperty -Name UserID -Value (
                        $AlliPilotUsers | 
                            Where-Object {$_.UserPrincipalName -eq $User.UserPrincipalName} | 
                                Select-Object -ExpandProperty UserID) -Force
                }
                
                # Add iPilotFirstName to ChangedUsers
                $User | Add-Member -MemberType NoteProperty -Name iPilotFirstName -Value (
                    $AlliPilotUsers | 
                        Where-Object {$_.UserPrincipalName -eq $User.UserPrincipalName} | 
                            Select-Object -ExpandProperty FirstName) -Force

                # Add iPilotLastName to ChangedUsers
                $User | Add-Member -MemberType NoteProperty -Name iPilotLastName -Value (
                    $AlliPilotUsers | 
                        Where-Object {$_.UserPrincipalName -eq $User.UserPrincipalName} | 
                            Select-Object -ExpandProperty LastName) -Force
            }

            # Export JSON with UserPrincipalName, UserID (iPilot) and objectGUID and check if saved JSON has a UPN change
            if ($SavediPilotUserMapping) {

                # Update saved iPilot User Mapping
                $SavediPilotUserMapping | ForEach-Object {
                    
                    # Update saved iPilot User Mapping
                    $ChangedUser = $ChangedUsers | Where-Object {$_.objectGUID -match $SavediPilotUserMapping.objectGUID}
                    if ($ChangedUser) {
                        $SavediPilotUserMapping  | Where-Object {$_.objectGUID -match $SavediPilotUserMapping.objectGUID}
                        Write-Verbose "Updating saved iPilot user mapping for $($ChangedUser.userPrincipalName). objectGUID: $($ChangedUser.objectGUID)"
                    }
                }
            }

            # Check for new users not in SavediPilotUserMapping
            $iPilotUserMapping = @()
            if ($SavediPilotUserMapping) {
                if($ChangedUsers) {
                    $Comparision = Compare-Object -ReferenceObject $SavediPilotUserMapping.objectGUID -DifferenceObject $ChangedUsers.objectGUID
                    $NewUsers = $Comparision | ForEach-Object {
                        if ($_.SideIndicator -eq "=>") {
                            $objectGUID = $_.InputObject
                            $ChangedUsers | Where-Object {$_.objectGUID -eq $objectGUID} | Select-Object userPrincipalName,UserID,objectGUID
                        }
                    }
                    $iPilotUserMapping += $SavediPilotUserMapping
                } else {
                    $iPilotUserMapping += $SavediPilotUserMapping
                    $iPilotUserMapping += $NewUsers
                }
            } else {
                # Store initial lookup of UPNs, UserIDs and objectGUIDs
                Write-Verbose "Store initial lookup of UPNs, UserIDs and objectGUIDs"
                $iPilotUserMapping += $ChangedUsers | Select-Object userPrincipalName,UserID,objectGUID
            }

            # Remove users
            if ($UsersRemovedFromiPilotSyncGroup) {
                $UsersRemovedFromiPilotSyncGroup | ForEach-Object {
                    Write-Verbose "Removing from iPilotUserMapping: $($_.userPrincipalName) ($($_.objectGUID))"
                    $iPilotUserMapping = $iPilotUserMapping | Where-Object {$_.objectGUID -ne $_.objectGUID}
                }
            }

            # Remove Duplicates
            Write-Debug "iPilotUserMapping (Pre-Select-Object -Unique *:`n$($iPilotUserMapping | Format-Table -AutoSize | Out-String)"
            $iPilotUserMapping = $iPilotUserMapping |Select-Object -Unique * # Requires asterisk for bug with Select-Object -Unique: https://github.com/PowerShell/PowerShell/issues/17953

            Write-Verbose "iPilotUserMapping:`n$($iPilotUserMapping | Format-Table -AutoSize | Out-String)"

            # Save iPilotUserMapping to JSON
            if ($iPilotUserMapping) {
                Write-Verbose "Saving iPilotUserMapping to $JsonPath"
                Write-Debug "`niPilotUserMapping saved to $($JsonPath):`n$($iPilotUserMapping | ConvertTo-Json)"
                $iPilotUserMapping | ConvertTo-Json | Out-File $JsonPath -Force
                Write-Verbose "Storing iPilotUserMapping in `$global:IP_SavediPilotUserMapping"
                $global:IP_SavediPilotUserMapping = $iPilotUserMapping | ConvertTo-Json
            }

            # Exit with success after initial sync
            if (!$SavediPilotUserMapping) {
                Write-Output "Exiting with success after initial sync"
                if(!$NoExit.IsPresent) {
                    Stop-Transcript -Verbose
                    Exit 0
                }
            }

        #endregion Get objectGuid to iPilot ID mapping

        #region Map objectGUID to UPN & UserID
        
            $iPilotUsersToUpdate = @()
            Foreach ($iPilotUser in $ChangedUsers) {

                # Azure AD Fist Name
                $iPilotUser | Add-Member -MemberType NoteProperty -Name AzureADFirstName -Value (
                    $AzureADUsers | 
                        Where-Object {$_.id -eq $iPilotUser.objectGUID -and $_.givenName} | 
                            Select-Object -First 1 -ExpandProperty givenName) -Force

                # Azure AD Last Name
                $iPilotUser | Add-Member -MemberType NoteProperty -Name AzureADLastName -Value (
                    $AzureADUsers | 
                        Where-Object {$_.id -eq $iPilotUser.objectGUID -and $_.surname} | 
                            Select-Object -First 1 -ExpandProperty surname) -Force

                # Azure AD UserPrincipalName
                $iPilotUser | Add-Member -MemberType NoteProperty -Name AzureADUserPrincipalName -Value (
                    $AzureADUsers | 
                        Where-Object {$_.id -eq $iPilotUser.objectGUID -and $_.userPrincipalName} | 
                            Select-Object -First 1 -ExpandProperty userPrincipalName) -Force

                # iPilot BusinessPhone
                if ($iPilotUser.businessPhones) {
                    $iPilotUser | Add-Member -MemberType NoteProperty -Name BusinessPhone -Value ($iPilotUser.businessPhones[0] -replace "[^0-9]",'') -Force
                }

                # iPilot First Name
                $iPilotUser | Add-Member -MemberType NoteProperty -Name iPilotFirstName -Value (
                    $AlliPilotUsers | 
                        Where-Object {$_.UserID -eq $iPilotUser.UserID -and $_.FirstName} | 
                            Select-Object -First 1 -ExpandProperty FirstName) -Force

                # iPilot Last Name
                $iPilotUser | Add-Member -MemberType NoteProperty -Name iPilotLastName -Value (
                    $AlliPilotUsers | 
                        Where-Object {$_.UserID -eq $iPilotUser.UserID -and $_.LastName} | 
                            Select-Object -First 1 -ExpandProperty LastName) -Force

                # iPilot UserPrincipalName
                $iPilotUser | Add-Member -MemberType NoteProperty -Name iPilotUserPrincipalName -Value (
                    $AlliPilotUsers | 
                        Where-Object {$_.UserID -eq $iPilotUser.UserID -and $_.UserPrincipalName} | 
                            Select-Object -First 1 -ExpandProperty UserPrincipalName) -Force

                # If iPilot First Name does not match Azure AD First Name, put user into array to be updated in iPilot
                if ($iPilotUser.AzureADFirstName -ne $iPilotUser.iPilotFirstName) {
                    $iPilotUsersToUpdate += $iPilotUser
                    Write-Output "First Name Changed - Adding $($iPilotUser.AzureADUserPrincipalName) to iPilotUsersToUpdate:`n$($iPilotUser | Format-List | Out-String)"
                }

                # If iPilot Last Name does not match Azure AD Last Name, put user into array to be updated in iPilot
                if ($iPilotUser.AzureADLastName -ne $iPilotUser.iPilotLastName) {
                    if($iPilotUsersToUpdate -notcontains $iPilotUser) {
                        $iPilotUsersToUpdate += $iPilotUser
                    }
                    Write-Output "Last Name Changed - Adding $($iPilotUser.AzureADUserPrincipalName) to iPilotUsersToUpdate:`n$($iPilotUser | Format-List | Out-String)"
                }

                # If iPilot UPN does not match Azure AD UPN, put user into array to be updated in iPilot
                if ($iPilotUser.AzureADUserPrincipalName -ne $iPilotUser.iPilotUserPrincipalName) {
                    if($iPilotUsersToUpdate -notcontains $iPilotUser) {
                        $iPilotUsersToUpdate += $iPilotUser
                    }
                    Write-Output "UPN Changed - Adding $($iPilotUser.AzureADUserPrincipalName) to iPilotUsersToUpdate:`n$($iPilotUser | Format-List | Out-String)"
                }

                # If iPilot number does not match Azure AD businessPhones, put user into array to be updated in iPilot
                if ($iPilotUser.iPilotUserPrincipalName) {
                    $iPilotUser | Add-Member -Name ExistingiPilotTelephoneNumber -MemberType NoteProperty -Value (
                        $AlliPilotUsers | Where-Object {$_.UserPrincipalName -eq $iPilotUser.iPilotUserPrincipalName} | Select-Object -ExpandProperty TelephoneNumber
                    ) -Force

                    if ($iPilotUser.BusinessPhone -ne $iPilotUser.ExistingiPilotTelephoneNumber) {
                        if($iPilotUsersToUpdate -notcontains $iPilotUser) {
                            $iPilotUsersToUpdate += $iPilotUser
                        }
                        Write-Output "BusinessPhone Changed - Adding $($iPilotUser.AzureADUserPrincipalName) to iPilotUsersToUpdate:`n$($iPilotUser | Format-List | Out-String)"
                    }
                }

                # Added to group
                if ($iPilotUser.AddedToiPilotSyncGroup) {
                    if($iPilotUsersToUpdate -notcontains $iPilotUser) {
                        $iPilotUsersToUpdate += $iPilotUser
                    }
                    Write-Output "Added to $iPilotSyncGroupName - Adding $($iPilotUser.AzureADUserPrincipalName) to iPilotUsersToUpdate:`n$($iPilotUser | Format-List | Out-String)"
                }
            }

        #endregion Map objectGUID to UPN & UserID

    #endregion Get user and group changes from Azure AD

    #region Perform sync

        #region Provision users from previous sync
            
            # Create array to store user accounts that will be written to disk for retreival on next sync
            $UsersToProvisionOnNextSync = @()

            # Retreive users to provision on this sync
            if (Test-Path "$iPilotDataDirectory\UsersToProvisionFromLastSync_$($iPilotSyncGroupNameNoSpaces)_$($global:IP_iPilotDomain).xml") {
                Write-Output "Retreiving users to provision on this sync from $iPilotDataDirectory\UsersToProvisionFromLastSync_$($iPilotSyncGroupNameNoSpaces)_$($global:IP_iPilotDomain).xml"
                $UsersToProvisionFromLastSync = Import-Clixml "$iPilotDataDirectory\UsersToProvisionFromLastSync_$($iPilotSyncGroupNameNoSpaces)_$($global:IP_iPilotDomain).xml"
            } else {
                Write-Output "Retreiving users to provision on this sync from `$global:IP_UsersToProvisionFromLastSync:`n$global:IP_UsersToProvisionFromLastSync"
                $UsersToProvisionFromLastSync = $global:IP_UsersToProvisionFromLastSync
            }

            # Provision users
            if ($UsersToProvisionFromLastSync) {
                Write-Verbose "UsersToProvisionFromLastSync:`n$($UsersToProvisionFromLastSync | Format-Table | Out-String)"
                
                Foreach ($iPilotUser in $UsersToProvisionFromLastSync) {
                    Write-Output "Provisioning $($iPilotUser.AzureADUserPrincipalName) from last sync with $($iPilotUser.BusinessPhone)"
                    $NewiPilotUserSplat = @{
                        UserPrincipalName = $iPilotUser.AzureADUserPrincipalName
                        FirstName = $iPilotUser.AzureADFirstName
                        LastName = $iPilotUser.AzureADLastName
                        TelephoneNumber = $iPilotUser.BusinessPhone
                    }
                    Try {
                        Write-Debug "NewiPilotUserSplat = $($NewiPilotUserSplat | Format-Table | Out-String)"
                        Try {
                            New-iPilotTeamsUserAssignment @NewiPilotUserSplat
                        } Catch {
                            Write-Error "Failed to provision $($iPilotUser.userPrincipalName). Adding user to UsersToProvisionOnNextSync.`nError:$($_.Exception.Message)"
                            $UsersToProvisionOnNextSync += $iPilotUser
                        }
                    } Catch {
                        Write-Error "Failed to provision $($iPilotUser.AzureADUserPrincipalName) with $($iPilotUser.BusinessPhone). Will retry next sync.`nError:$($_)"
                        $UsersToProvisionOnNextSync += $iPilotUser
                    }
                }
            } else {
                Write-Output "No users to provision found in $iPilotDataDirectory\UsersToProvisionFromLastSync_$($iPilotSyncGroupNameNoSpaces)_$($global:IP_iPilotDomain).xml or `$global:IP_UsersToProvisionFromLastSync"
            }

        #endregion Provision users from previous sync

        #region Provision new users/update users with name or number changes

            # Output to log if no users to update
            if (!$iPilotUsersToUpdate) {
                Write-Output "No users to update"
            } else {
                Write-Verbose "Provision new users/update users with name or number changes:`n$($iPilotUsersToUpdate | Format-List | Out-String)`n`n"
            }

            Foreach ($iPilotUser in $iPilotUsersToUpdate) {

                #region Added to Group

                    # if iPilotUserPrincipalName is not populated, user needs to be provisioned
                    if (!$iPilotUser.iPilotUserPrincipalName) {
                        Write-Verbose "$($iPilotUser.AzureADUserPrincipalName)'s iPilotUserPrincipalName is blank. Will provision account."
                    } else {
                        $ProvisionUser = $false
                    }

                    if ($iPilotUser.AddedToiPilotSyncGroup -or $ProvisionUser) {

                        #region Provision User

                            if (!$iPilotUser.iPilotUserPrincipalName -and !$iPilotUser.UserID -and $iPilotUser.accountEnabled -eq $true) { 
                                Write-Output "Provisioning new user:
                                                UserPrincipalName: $($iPilotUser.AzureADUserPrincipalName)
                                                FirstName: $($iPilotUser.AzureADFirstName)
                                                LastName: $($iPilotUser.AzureADLastName)"

                                Write-Debug "iPilotUserPrincipalName and iPilot UserID are empty"

                                # Check if user has existing businessPhone attribute
                                if ($iPilotUser.BusinessPhone) {
                                    Write-Debug "$($iPilotUser.AzureADUserPrincipalName) iPilotUser.BusinessPhone = $($iPilotUser.BusinessPhone)"

                                    # Check if BusinessPhone is an available number in iPilot
                                    Try {
                                        if ($iPilotUser.BusinessPhone.Length -eq 11) {
                                            $TelephoneNumber = Get-iPilotNumber -FilterByTelephoneNumber $iPilotUser.BusinessPhone.Substring(1)
                                        } else {
                                            $TelephoneNumber = Get-iPilotNumber -FilterByTelephoneNumber $iPilotUser.BusinessPhone
                                        }
                                        Write-Verbose "TelephoneNumber to use for $($iPilotUser.AzureADUserPrincipalName):`n$($TelephoneNumber | Format-List | Out-String)"
                                    } Catch {
                                        Write-Warning "Failed to retreive matching phone number from iPilot for $($iPilotUser.AzureADUserPrincipalName). Attempted $($iPilotUser.BusinessPhone) & $($iPilotUser.BusinessPhone.Substring(1))."
                                    }
                                   
                                    # BusinessPhone (AzureAD) matches number in iPilot (TelephoneNumber)
                                    if ($TelephoneNumber) {

                                        # If phone number was changed and number exists unassigned in iPilot, auto deprovision the old number (if one), and auto provision the new number
                                        if (!$TelephoneNumber.ProvisionUser.upn) { # If TelephoneNumber is not assigned to another user
                            
                                            Write-Debug "TelephoneNumber.ProvisionUser.upn is blank"
                                            Try {
                                                $CurrentiPilotTelephoneNumber = Get-iPilotTeamsUser -FilterByUserPrincipalName $iPilotUser.AzureADUserPrincipalName | Select-Object -ExpandProperty TelephoneNumber
                                            } Catch {
                                                Write-Verbose "No existing iPilot TelephoneNumber assigned."
                                                Write-Debug "`$CurrentiPilotTelephoneNumber = Get-iPilotTeamsUser -FilterByUserPrincipalName $($iPilotUser.AzureADUserPrincipalName) | Select-Object -ExpandProperty TelephoneNumber"
                                            }
                                            Write-Verbose "CurrentiPilotTelephoneNumber: $($CurrentiPilotTelephoneNumber | Format-List | Out-String)"
                            
                                            # Number is not assigned, user doesn't have existing number assigned, assigning matching number found in iPilot
                                            if (!$CurrentiPilotTelephoneNumber) {

                                                # Assign BusinessPhone as iPilot number
                                                Write-Output "Provisioning $($iPilotUser.AzureADUserPrincipalName) with $($TelephoneNumber.TelephoneNumber)"
                                                $NewiPilotUserSplat = @{
                                                    UserPrincipalName = $iPilotUser.AzureADUserPrincipalName
                                                    FirstName = $iPilotUser.AzureADFirstName
                                                    LastName = $iPilotUser.AzureADLastName
                                                    TelephoneNumber = $TelephoneNumber.TelephoneNumber
                                                    Credential = $iPilotCredential
                                                }
                                                Write-Debug "NewiPilotUserSplat = $($NewiPilotUserSplat | Format-Table | Out-String)"
                                                Try {
                                                    New-iPilotTeamsUserAssignment @NewiPilotUserSplat
                                                } Catch {
                                                    Write-Error "Failed to provision $($iPilotUser.AzureADUserPrincipalName). Adding user to UsersToProvisionOnNextSync.`nError:$($_.Exception.Message)"
                                                    $UsersToProvisionOnNextSync += $iPilotUser
                                                }

                                            # Number is not assigned, user has existing number assigned, will deprovision and reprovision with new number
                                            } elseif ($CurrentiPilotTelephoneNumber.Length -eq 10 -and $TelephoneNumber.TelephoneNumber) {
                                
                                                # Deprovision existing number
                                                Remove-iPilotTeamsUserAssignment -UserPrincipalName $iPilotUser.AzureADUserPrincipalName

                                                # Set businessPhone to be used on next sync
                                                $iPilotUser.businessPhone = $TelephoneNumber.TelephoneNumber

                                                # Add user to UsersToProvisionOnNextSync
                                                $UsersToProvisionOnNextSync += $iPilotUser
                                            }

                                        } else { # If phone number was changed and new number doesn't exist or is already assigned in iPilot, do nothing
                                            Write-Warning "Would have assigned $($TelephoneNumber.TelephoneNumber) to $($iPilotUser.AzureADUserPrincipalName) but it is already assigned to $($TelephoneNumber.ProvisionUser.upn). Doing nothing."
                                        }
                                    } else { # If phone number is present in AD and does NOT match one in iPilot, do nothing
                                        Write-Warning "$($iPilotUser.AzureADUserPrincipalName) BusinessPhone attribute is present but does not match number in iPilot, skipping provisioning"
                                    }
                                } else { # Business Phone blank, get next available number & provision user

                                    # Assign next available number
                                    Write-Verbose "$($iPilotUser.AzureADUserPrincipalName)'s BusinessPhone is blank, get next available number & provision user"
                                    if ($FilterByTelephoneNumber) {
                                        $TelephoneNumber = Get-iPilotNumber -Available -FilterByTelephoneNumber $FilterByTelephoneNumber -NumberOfRecords 1 | Where-Object {$_.ProvisionUserStatus -eq "PENDING"} | Select-Object -First 1
                                    } else {
                                        $TelephoneNumber = Get-iPilotNumber -Available -NumberOfRecords 1 | Where-Object {$_.ProvisionUserStatus -eq "PENDING"} | Select-Object -First 1
                                    }

                                    if ($TelephoneNumber.Status -eq "COMPLETE" -and $TelephoneNumber.ProvisionUserStatus -eq "PENDING") {
                                        # Provision user with first available TelephoneNumber
                                        Write-Output "Provisioning $($iPilotUser.AzureADUserPrincipalName) with $($TelephoneNumber.TelephoneNumber)"
                                        Try {
                                            $NewiPilotUserSplat = @{
                                                UserPrincipalName = $iPilotUser.AzureADUserPrincipalName
                                                FirstName = $iPilotUser.AzureADFirstName
                                                LastName = $iPilotUser.AzureADLastName
                                                TelephoneNumber = $TelephoneNumber.TelephoneNumber
                                                Credential = $iPilotCredential
                                            }
                                            New-iPilotTeamsUserAssignment @NewiPilotUserSplat
                                        } Catch {
                                            Write-Error "Failed to provision $($iPilotUser.AzureADUserPrincipalName). Adding user to UsersToProvisionOnNextSync.`nError:$($_.Exception.Message)"
                                            $UsersToProvisionOnNextSync += $iPilotUser
                                        }
                                    } else {
                                        # No numbers in iPilot ready to be used for for provisioning, skipping provisioning
                                        Write-Warning "No numbers in iPilot ready to be used for for provisioning, skipping provisioning for $($iPilotUser.AzureADUserPrincipalName)"
                                    }
                                }
                            }

                        #endregion Provision User
                    }

                #endregion Added to Group

                #region Update user's First/Last Name for existing users

                    if ($iPilotUser.iPilotUserPrincipalName -and ( 
                                ($iPilotUser.AzureADFirstName -ne $iPilotUser.iPilotFirstName) -or ($iPilotUser.AzureADLastName -ne $iPilotUser.iPilotLastName)
                            )
                        ) {

                        # Splat Set-iPilotTeamsUser
                        Write-Output "=== Updating UserID $($iPilotUser.UserID): ===
                                        UserPrincipalName: $($iPilotUser.iPilotUserPrincipalName)
                                        NewUserPrincipalName: $($iPilotUser.AzureADUserPrincipalName)
                                        FirstName: $($iPilotUser.AzureADFirstName)
                                        LastName: $($iPilotUser.AzureADLastName)`n"


                        # Set UPN with First and Last Name
                        $SetiPilotUserSplat = @{
                            UserPrincipalName = $iPilotUser.iPilotUserPrincipalName
                            FirstName = $iPilotUser.AzureADFirstName
                            LastName = $iPilotUser.AzureADLastName
                        }

                        $SetiPilotTeamsUserResponse = Set-iPilotTeamsUser @SetiPilotUserSplat
                        if ($SetiPilotTeamsUserResponse.statusCode -eq 200) {
                            Get-iPilotTeamsUser -FilterByUserPrincipalName $iPilotUser.iPilotUserPrincipalName
                        } else {
                            throw "Failed to update User ID $($iPilotUser.UserID) - $($iPilotUser.AzureADFirstName) $($iPilotUser.AzureADLastName). Error: $($SetiPilotTeamsUserResponse.statusCode) - $($SetiPilotTeamsUserResponse.status)"
                        }
                    }

                #endregion Update user's First/Last Name for existing users

                #region If still in group, and number changes to unassigned number in ipilot, deprovision original number, and provision new number
                    
                    if ($iPilotUser.iPilotUserPrincipalName -and ($iPilotSyncGroupMembers.userPrincipalName -contains $iPilotUser.AzureADUserPrincipalName)) {
                        Write-Verbose "If still in group, and number changes to unassigned number in ipilot, deprovision original number, and provision new number"
                        # Check if number changed
                        if ($iPilotUser.BusinessPhone -and ($iPilotBusinessPhoneNumber.TelephoneNumber -ne $iPilotUser.BusinessPhone)) {
                            $NewTelephoneNumber = Get-iPilotNumber -FilterByTelephoneNumber $iPilotUser.BusinessPhone
                            Write-Verbose "$($iPilotUser.AzureADUserPrincipalName) NewTelephoneNumber:`n$($NewTelephoneNumber | Format-Table -AutoSize | Out-String)"
                            $OldTelephoneNumber = Get-iPilotNumber -FilterByTelephoneNumber $iPilotUser.ExistingiPilotTelephoneNumber
                            Write-Verbose "$($iPilotUser.AzureADUserPrincipalName) OldTelephoneNumber:`n$($OldTelephoneNumber | Format-Table -AutoSize | Out-String)"
                        }

                        # If number changed
                        if (($NewTelephoneNumber.TelephoneNumber -ne $iPilotUser.ExistingiPilotTelephoneNumber) -and $NewTelephoneNumber.ProvisionUserStatus -eq "PENDING") {
                            
                            Write-Output "Deprovisioning $($iPilotUser.AzureADUserPrincipalName) from $($iPilotUser.ExistingiPilotTelephoneNumber)"
                            Remove-iPilotTeamsUserAssignment -UserPrincipalName $iPilotUser.AzureADUserPrincipalName
                            Write-Verbose "Waiting 30 seconds..."
                            Start-Sleep -Seconds 30
                            $ProvisionStatus = Get-iPilotNumber -FilterByUserPrincipalName $iPilotUser.AzureADUserPrincipalName | Select-Object -ExpandProperty ProvisionUserStatus
                            Write-Verbose "$($iPilotUser.AzureADUserPrincipalName) - TelephoneNumber $($iPilotUser.ExistingiPilotTelephoneNumber) ProvisionStatus: $ProvisionStatus"

                            # Provision user if successfully deprovisioned
                            if ($ProvisionStatus -ne "PENDING") {
                                Write-Warning "$($iPilotUser.AzureADUserPrincipalName) is not deprovisioned. Will wait until next sync to provision."
                                $UsersToProvisionOnNextSync += $iPilotUser
                            } else {
                                
                                if ($NewTelephoneNumber.ProvisionUserStatus -eq "PENDING") {
                                    # Provision user with new phone number
                                    $NewiPilotUserSplat = @{
                                        UserPrincipalName = $iPilotUser.AzureADUserPrincipalName
                                        FirstName = $iPilotUser.AzureADFirstName
                                        LastName = $iPilotUser.AzureADLastName
                                        TelephoneNumber = $NewTelephoneNumber.TelephoneNumber
                                        Credential = $iPilotCredential
                                    }
                                    Write-Output "Provisioning $($iPilotUser.AzureADUserPrincipalName) with new phone number ($($NewTelephoneNumber.TelephoneNumber))"
                                    Write-Debug "NewiPilotUserSplat = $($NewiPilotUserSplat | Format-Table | Out-String)"
                                    Try {
                                        New-iPilotTeamsUserAssignment @NewiPilotUserSplat
                                    } Catch {
                                        Write-Error "Failed to provision $($iPilotUser.AzureADUserPrincipalName). Adding user to UsersToProvisionOnNextSync.`nError:$($_.Exception.Message)"
                                        $UsersToProvisionOnNextSync += $iPilotUser
                                    }
                                } else {
                                    Write-Warning "$($iPilotUser.BusinessPhone) is not available in iPilot. ProvisionUserStatus: $($NewTelephoneNumber.ProvisionUserStatus)"
                                }
                            }
                        } else {
                            Write-Verbose "$($iPilotUser.AzureADUserPrincipalName) phone number has not changed"
                        }
                    }

                #endregion If still in group, and number changes to unassigned number in ipilot, deprovision original number, and provision new number

            }

            # Export UsersToProvisionOnNextSync to UsersToProvisionFromLastSync_$($iPilotSyncGroupNameNoSpaces)_<domain>.xml in iPilot data directory for retreival on next sync
            $UsersToProvisionOnNextSync | Export-Clixml "$iPilotDataDirectory\UsersToProvisionFromLastSync_$($iPilotSyncGroupNameNoSpaces)_$($global:IP_iPilotDomain).xml" -Force
            Write-Verbose "UsersToProvisionOnNextSync:`n$($UsersToProvisionOnNextSync | Format-Table | Out-String)"
            $global:IP_UsersToProvisionFromLastSync = $UsersToProvisionOnNextSync

        #endregion Provision new users/update users with name or number changes

        #region Update users with UPN Changes

            if ($ChangedUserPrincipalNames) {
                Write-Output "Updating users with UserPrincipalName changes"
                Foreach ($iPilotUser in $ChangedUserPrincipalNames) {
                    Write-Output "Updating UserID $($iPilotUser.UserID):
                        UserPrincipalName: $($iPilotUser.PreviousUPN)
                        NewUserPrincipalName: $($iPilotUser.userPrincipalName)
                        FirstName: $($iPilotUser.AzureADFirstName)
                        LastName: $($iPilotUser.AzureADLastName)"


                    Try {
                        $SetiPilotUserSplat = @{
                            UserPrincipalName = $iPilotUser.PreviousUPN
                            NewUserPrincipalName = $iPilotUser.userPrincipalName
                            FirstName = $iPilotUser.AzureADFirstName
                            LastName = $iPilotUser.AzureADLastName
                        }
                        Set-iPilotTeamsUser @SetiPilotUserSplat -ErrorAction Stop
                    } Catch {
                        Write-Warning "Failed to update the $($iPilotUser.PreviousUPN) to $($iPilotUser.userPrincipalName) in iPilot. User will be unassigned and re-assigned using $($iPilotUser.ExistingiPilotTelephoneNumber).`Error:$($_.Exception.Message)"
                        
                        Write-Output "UPN Change - Removing $($iPilotUser.PreviousUPN)..."
                        Try {
                            Remove-iPilotTeamsUserAssignment -UserPrincipalName $iPilotUser.PreviousUPN -Wait
                        } Catch {
                            Write-Error "Failed to de-provision $($iPilotUser.userPrincipalName) after removing their previous UPN $($iPilotUser.PreviousUPN). $($_.Exception.Message)"
                        }

                        Write-Output "UPN Change - Re-provisioning $($iPilotUser.PreviousUPN) with same number ($($iPilotUser.ExistingiPilotTelephoneNumber)) previously assigned to $($iPilotUser.userPrincipalName)..."
                        Try {
                            $UPNUpdateSplat = @{
                                UserPrincipalName = $iPilotUser.userPrincipalName
                                FirstName = $iPilotUser.AzureADFirstName
                                LastName = $iPilotUser.AzureADLastName
                                TelephoneNumber = $iPilotUser.ExistingiPilotTelephoneNumber
                                Credential = $iPilotCredential
                            }
                            New-iPilotTeamsUserAssignment -UserPrincipalName $iPilotUser.userPrincipalName -ErrorAction Stop
                        } Catch {
                            Write-Error "Failed to re-provision $($iPilotUser.userPrincipalName) after removing their previous UPN $($iPilotUser.PreviousUPN). $($_.Exception.Message)"
                        }
                    }
                }
            } else {
                Write-Output "No users have UserPrincipalName changes"
            }

        #endregion Update users with UPN Changes

        #region Users removed from sync group

            if ($AlliPilotUsers) {
                $DeletedUsers = @()
                $DeletedUsers += $ChangedUsers | Where-Object {$_.RemovedFromiPilotSyncGroup -eq $true}
                
                # Get any users that aren't in iPilot sync group and add them to group to be deprovisioned
                $UserPrincipalNamesRemovedFromiPilotSyncGroup = Compare-Object -ReferenceObject $iPilotSyncGroupMembers.userPrincipalName `
                    -DifferenceObject $AlliPilotUsers.UserPrincipalName | 
                        ForEach-Object { $_ | 
                            Where-Object {$_.SideIndicator -eq "=>"}} | 
                                Select-Object -ExpandProperty InputObject
                $UsersRemovedFromiPilotSyncGroup += $ChangedUsers | Where-Object {$UserPrincipalNamesRemovedFromiPilotSyncGroup -contains $_.userPrincipalName }

                # Add missing attributes
                if ($UsersRemovedFromiPilotSyncGroup) {
                    $UsersRemovedFromiPilotSyncGroup | ForEach-Object { Add-Member -InputObject $_ -Name BusinessPhone -MemberType NoteProperty -Value $_.TelephoneNumber -Force}
                    $UsersRemovedFromiPilotSyncGroup | ForEach-Object { Add-Member -InputObject $_ -Name AzureADUserPrincipalName -MemberType NoteProperty -Value $_.UserPrincipalName -Force}
                }

                # Output users removed from iPilot Sync Group
                Write-Verbose "UsersRemovedFromiPilotSyncGroup:`n$($UsersRemovedFromiPilotSyncGroup | Format-List | Out-String)"
                Write-Verbose "Adding UsersRemovedFromiPilotSyncGroup to DeletedUsers"
                $DeletedUsers += $UsersRemovedFromiPilotSyncGroup
                Write-Debug "`nDeletedUsers:`n$($DeletedUsers | Format-List | Out-String)"

                if ($DeletedUsers) {
                    Write-Output "Deprovisioning users removed from $($iPilotSyncGroupNameNoSpaces):`n$($DeletedUsers | Format-List | Out-String)"
                } else {
                    Write-Output "No users removed from $iPilotSyncGroupName"
                }

                Foreach ($DeletedUser in $DeletedUsers) {
                    Write-Debug "`nDeletedUser:`n$($DeletedUser)"
                    
                    # iPilot BusinessPhone
                    if ($DeletedUser.businessPhones -and !$DeletedUser.BusinessPhone) {
                        Write-Verbose "BusinessPhone property is empty, but user has businessPhones populated, setting BusinessPhone property to:$(($DeletedUser.businessPhones[0] -replace "[^0-9]",''))"
                        $DeletedUser | Add-Member -MemberType NoteProperty -Name BusinessPhone -Value ($DeletedUser.businessPhones[0] -replace "[^0-9]",'') -Force
                    }

                    # If phone number is not present in AD and UPN matches an assigned number in iPilot, auto deprovision user
                    $DeletedUseriPilotUserPrincipalName = Get-iPilotNumber -FilterByUserPrincipalName $DeletedUser.AzureADUserPrincipalName
                    if ($DeletedUseriPilotUserPrincipalName) {
                        Write-Output "Deprovisioning $($DeletedUser.AzureADUserPrincipalName) - user was removed from $iPilotSyncGroupName"
                        $RemoveiPilotTeamsUserAssignmentSplat = @{
                            UserPrincipalName = $DeletedUser.AzureADUserPrincipalName
                        }
                        if ($VerbosePreference = "Continue") {
                            $RemoveiPilotTeamsUserAssignmentSplat += @{Verbose = $true}
                        }
                        if ($DebugPreference = "Continue") {
                            $RemoveiPilotTeamsUserAssignmentSplat += @{Debug = $true}
                        }
                        Remove-iPilotTeamsUserAssignment @RemoveiPilotTeamsUserAssignmentSplat
                    }
                }
            }

        #endregion Users removed from sync group

        #region Disabled users - deprovision

            $DisabledUsers = $ChangedUsers | Where-Object {$_.accountEnable -eq $false}
            if ($DisabledUsers) {
                Write-Output "Deprovisioning users disabled in Azure AD"
                Foreach ($DisabledUser in $DisabledUsers) {
                    Write-Output "Deprovisioning $($DisabledUser.AzureADUserPrincipalName) - user disable in Azure AD"
                    Remove-iPilotTeamsUserAssignment -UserPrincipalName $DisabledUser.AzureADUserPrincipalName
                }
            } else {
                Write-Output "No users in $iPilotSyncGroupName have been disabled in Azure AD"
            }
            
        #endregion Disabled users - deprovision

    #endregion Perform sync

    #region End Logging

        $LogsToDelete = Get-ChildItem -Path $iPilotDataDirectory -Filter *_Invoke-iPilotUserSync.log -Depth 0  | Where-Object {$_.LastWriteTime -lt (Get-Date).AddDays(-$RetainLogsForDays)}
        if ($LogsToDelete) {
            Write-Output "Cleaning up logs older than $RetainLogsForDays in $iPilotDataDirectory"

            Write-Output "Deleting Log files:`n$($LogsToDelete | Format-Table | Out-String)"
            If ($LogsToDelete.Count -ge 1) {
                $LogsToDelete | Remove-Item -Force -Verbose
            }
        }

        # Verbose Switch
        if($PSBoundParameters.containskey("Verbose")) {
            $VerbosePreference = $PreviousVerbosePreference
        }

        # Debug Switch
        if($PSBoundParameters.containskey("Debug")) {
            $DebugPreference = $PreviousDebugPreference
        }

        Stop-Transcript -Verbose

        # Store logs in global variable
        $global:IP_iPilotInvokeUserSyncLog = Get-Content -Path $LogFile

    #endregion End Logging
    
}