Public/Invoke-iPilotUserSync.ps1

Function Invoke-iPilotUserSync {
    Param (
        [System.String]  [Parameter(Mandatory = $false)]
            $iPilotSyncGroupName = "NuWave iPilot Assigned Phone Numbers",
        [System.String] [Parameter(Mandatory = $true)] 
            $iPilotUsername,
        [System.String] [Parameter(Mandatory = $false)]
            $iPilotDataDirectory = "${env:APPDATA}\NuWave",
        [System.Int16] [Parameter(Mandatory = $false)]
            $RetainLogsForDays = 7,
        [System.String] [Parameter(Mandatory = $false)]
            $ApiUrl = "https://api.nuwave.com",
        [System.String] [Parameter(Mandatory = $false)]
            $Instance
    )

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

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

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

    #region Start Logging

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

    #endregion Start Logging

    #region Store/Retreive Credentials

        $InitializeiPilotSessionSplat = @{}

        # Add Instance to splat
        if ($Instance) {
            $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
        Get-iPilotTeamsDomain -Credential $global:IP_iPilotCredential

        #region Get Azure AD OAuth Application Token for Graph API
        
            # Get Token
            Get-iPilotDirectorySyncOAuthToken

            # Set Header
            $AzureADGraphHeaders = @{
                Authorization = "Bearer $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
            $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

            #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 $iPilotDirectorySyncOAuthToken"") -ContentType ""application/json""`n"
                } Catch {
                    Write-Error "Failed to get members of $iPilotSyncGroupName. Exiting"
                    #Exit 1
                }
                $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 $iPilotDirectorySyncOAuthToken"") -ContentType ""application/json""`n"
                        } Catch {
                            Write-Error "Failed to pull initial list of all Azure AD Users. Exiting"
                            #Exit 1
                        }

                        $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-Verbose "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 $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 ",")"
                        $iPilotSyncGroupMember | Add-Member -MemberType NoteProperty -Name businessPhone -Value $iPilotSyncGroupMemberProperties.businessPhones[0] -Force

                        # Drop all but last 10 digits if number exceeds 10 digits
                        if ($iPilotSyncGroupMember.businessPhone.length -gt 10) {
                            Write-Verbose "$($iPilotSyncGroupMember.userPrincipalName) - businessPhone length exceeds 10 digits. Actual value: $($iPilotSyncGroupMember.businessPhone)"
                            $iPilotSyncGroupMember | Add-Member -MemberType NoteProperty -Name businessPhone -Value $iPilotSyncGroupMember.businessPhone.Substring($iPilotSyncGroupMember.businessPhone.Length - 10) -Force
                            Write-Verbose "$($iPilotSyncGroupMember.userPrincipalName) - businessPhone new value: $($iPilotSyncGroupMember.businessPhone)"
                        }
                    } 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)"
                        $iPilotSyncGroupMember | Add-Member -MemberType NoteProperty -Name ipPhone -Value $iPilotSyncGroupMemberProperties.ipPhone
                    } 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)"
                        $iPilotSyncGroupMember | Add-Member -MemberType NoteProperty -Name telephoneNumber -Value $iPilotSyncGroupMemberProperties.telephoneNumber
                    } 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_$($iPilotDomain).txt")) {

                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"

                # 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 $iPilotDirectorySyncOAuthToken"") -ContentType ""application/json""`n"
                } Catch {
                    Write-Error "Failed to pull initial list of all Azure AD Users. Exiting. Error Message: $($_.Exception.Message)"
                    #Exit 1
                }

                # 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 $iPilotDirectorySyncOAuthToken"") -ContentType ""application/json""`n"
                        } Catch {
                            Write-Error "Failed to pull initial list of all Azure AD Users. Exiting"
                            #Exit 1
                        }

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

                # Output next link to deltaLink file for baseline
                if ($GetDirectoryObjectsInitialRequest.'@odata.deltaLink') {
                    Write-Output "Output next link to $("$iPilotDataDirectory\UserDeltaLink_$($iPilotDomain).txt") for next sync`n"
                    $GetDirectoryObjectsInitialRequest.'@odata.deltaLink' | Out-File "$iPilotDataDirectory\UserDeltaLink_$($iPilotDomain).txt" -Force
                } 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 file
                $GetDirectoryObjectsDeltaUri = Get-Content -Path "$iPilotDataDirectory\UserDeltaLink_$($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 $iPilotDirectorySyncOAuthToken"") -ContentType ""application/json""`n"
                } Catch {
                    Write-Error "Failed to pull delta list of all Azure AD Users. Exiting"
                    #Exit 1
                }

                # 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 $iPilotDirectorySyncOAuthToken"") -ContentType ""application/json""`n"
                        } Catch {
                            Write-Error "Failed to pull delta list of all Azure AD Users. Exiting"
                            #Exit 1
                        }

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

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

                # Output next link to deltaLink file for next delta sync
                if ($GetDirectoryObjectsDeltaRequest.'@odata.deltaLink') {
                    Write-Output "Output next link to $("$iPilotDataDirectory\UserDeltaLink_$($iPilotDomain).txt") for next sync`n"
                    $GetDirectoryObjectsDeltaRequest.'@odata.deltaLink' | Out-File "$iPilotDataDirectory\UserDeltaLink_$($iPilotDomain).txt" -Force
                } 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 directory
            if (!(Test-Path -Path "$iPilotDataDirectory\GroupDeltaLink_$($iPilotDomain).txt")) {

                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 "Invoke-RestMethod -Method Get -Uri $GetDirectoryGroupInitialUri -Headers @(Authorization = ""Bearer $iPilotDirectorySyncOAuthToken"") -ContentType ""application/json"""
                } Catch {
                    Write-Error "Failed to perform Initial Lookup of $iPilotSyncGroupName Members. Exiting"
                    #Exit 1
                }

                # Get initial values
                $iPilotSyncGroupMemberIds = $GetDirectoryGroupInitialRequest.Value.'members@delta'.id

                # 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 "Invoke-RestMethod -Method Get -Uri $($GetDirectoryGroupInitialRequest.'@odata.nextLink') -Headers @(Authorization = ""Bearer $iPilotDirectorySyncOAuthToken"") -ContentType ""application/json"""
                        } Catch {
                            Write-Error "Failed to perform initial lookup of $iPilotSyncGroupName Members. Exiting"
                            #Exit 1
                        }

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

                # Output deltaLink to file for next sync
                if ($GetDirectoryGroupInitialRequest.'@odata.deltaLink') {
                    Write-Output "Output next link to $("$iPilotDataDirectory\GroupDeltaLink_$($iPilotDomain).txt") for next sync"
                    $GetDirectoryGroupInitialRequest.'@odata.deltaLink' | Out-File "$iPilotDataDirectory\GroupDeltaLink_$($iPilotDomain).txt" -Force
                } 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 file
                $GetDirectoryGroupDeltaUri = Get-Content -Path "$iPilotDataDirectory\GroupDeltaLink_$($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 $iPilotDirectorySyncOAuthToken"") -ContentType ""application/json"""
                } Catch {
                    Write-Error "Failed to perform delta lookup of $iPilotSyncGroupName Members. Exiting"
                    #Exit 1
                }

                # Get delta values
                $iPilotSyncGroupMemberIds = $GetDirectoryGroupDeltaRequest.Value.'members@delta'.id

                # 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 $iPilotDirectorySyncOAuthToken"") -ContentType ""application/json"""
                        } Catch {
                            Write-Error "Failed to perform delta lookup of $iPilotSyncGroupName Members. Exiting"
                            #Exit 1
                        }

                        $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-Verbose "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 {

                            $UserRemovedFromiPilotSyncGroup = $_
                            Write-Output "RemovediPilotSyncGroupMemberId: $($_.id)"

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

                                #region Get user's details

                                    $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 $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-Output "Adding $($RemovediPilotSyncGroupMember.userPrincipalName) to UsersRemovedToiPilotSyncGroup"

                                # 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_$($iPilotDomain).txt") for next sync"
                    $GetDirectoryGroupDeltaRequest.'@odata.deltaLink' | Out-File "$iPilotDataDirectory\GroupDeltaLink_$($iPilotDomain).txt" -Force
                } 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

            Write-Output "Getting changed users from last sync that are in $iPilotSyncGroupName"
            $ChangedUsers = @()
            $ChangedUsers += $AzureADUsers | Where-Object {
                $ItemToCheck = $_
                $iPilotSyncGroupMembers | Where-Object { $ItemToCheck -match $_.id }
            }

            # Add UsersRemovedFromiPilotSyncGroup to ChangedUsers
            $ChangedUsers += $UsersRemovedFromiPilotSyncGroup
            $ChangedUsers = $ChangedUsers | Where-Object {$_} # Filters out any empty items

            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_$($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
                Write-Verbose "SavediPilotUserMapping:`n$($SavediPilotUserMapping | Format-Table -AutoSize | Out-String)"
            }

            # 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 -Verbose -ErrorAction Stop
                    $AlliPilotUsers = Get-iPilotNumber -UserPrincipalNameAssigned | Where-Object {$_.UserPrincipalName}
                } Catch {
                    Write-Error "Failed to retreive all iPilot users. Exiting with failure.`nError: $($_.Exception.Message)"
                    Exit 1
                }
            }
    
            # 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
                $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) {
                $ChangedUserPrincipalNames = @()
                Foreach ($SavediPilotUser in $SavediPilotUserMapping) {

                    # Check if user is in saved JSON
                    $CurrentiPilotUser = $ChangedUsers | Where-Object {$_.objectGUID -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 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
            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 + $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
            }
            Write-Verbose "iPilotUserMapping:`n$($iPilotUserMapping | Format-Table -AutoSize | Out-String)"

            # Save iPilotUserMapping to JSON
            $iPilotUserMapping | ConvertTo-Json | Out-File $JsonPath -Force -Verbose

            # Exit with success after initial sync
            if (!$SavediPilotUserMapping) {
                Write-Output "Exiting with success after initial sync"
                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} | 
                            Select-Object -ExpandProperty givenName) -Force

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

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

                # iPilot BusinessPhone
                if ($iPilotUser.businessPhones) {
                    $iPilotUser | Add-Member -MemberType NoteProperty -Name BusinessPhone -Value (
                        $iPilotUser.businessPhones.Substring($iPilotUser.businessPhones[0].Length - 10)) -Force
                }

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

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

                # iPilot UserPrincipalName
                $iPilotUser | Add-Member -MemberType NoteProperty -Name iPilotUserPrincipalName -Value (
                    $AlliPilotUsers | 
                        Where-Object {$_.UserID -eq $iPilotUser.UserID} | 
                            Select-Object -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_$($iPilotDomain).xml") {
                Write-Output "Retreiving users to provision on this sync from $iPilotDataDirectory\UsersToProvisionFromLastSync_$($iPilotDomain).xml"
                $UsersToProvisionFromLastSync = Import-Clixml "$iPilotDataDirectory\UsersToProvisionFromLastSync_$($iPilotDomain).xml"

                # Provision users
                if ($UsersToProvisionFromLastSync) {
                    
                    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 {
                            New-iPilotTeamsUserAssignment @NewiPilotUserSplat
                        } 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_$($iPilotDomain).xml"
                }
            }

        #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"
            }

            Foreach ($global:IP_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."
                        $ProvisionUser = $true
                    } 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:`n UserPrincipalName: $($iPilotUser.AzureADUserPrincipalName)`n FirstName: $($iPilotUser.AzureADFirstName)`n LastName: $($iPilotUser.AzureADLastName)"
                                Write-Debug "iPilotUserPrincipalName and iPilot UserID are empty"

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

                                    # Check if BusinessPhone is an available number in iPilot
                                    $TelephoneNumber = Get-iPilotNumber -FilterByTelephoneNumber $iPilotUser.BusinessPhone -iPilotDomain $iPilotDomain -Credential $iPilotCredential -Verbose
                                    Write-Verbose "TelephoneNumber:`n$($TelephoneNumber | Format-List | Out-String)"

                                    # 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
                                                }
                                                New-iPilotTeamsUserAssignment @NewiPilotUserSplat

                                            # 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-Verbose "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"
                                    $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)"
                                        $NewiPilotUserSplat = @{
                                            UserPrincipalName = $iPilotUser.AzureADUserPrincipalName
                                            FirstName = $iPilotUser.AzureADFirstName
                                            LastName = $iPilotUser.AzureADLastName
                                            TelephoneNumber = $TelephoneNumber.TelephoneNumber
                                            Credential = $iPilotCredential
                                        }
                                        New-iPilotTeamsUserAssignment @NewiPilotUserSplat
                                    } 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): ===`n UserPrincipalName: $($iPilotUser.iPilotUserPrincipalName)`n `NewUserPrincipalName: $($iPilotUser.AzureADUserPrincipalName)`n FirstName: $($iPilotUser.AzureADFirstName)`n 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)) {

                        # 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 -Verbose | 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
                                    }
                                    New-iPilotTeamsUserAssignment @NewiPilotUserSplat
                                } 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_<domain>.xml in iPilot data directory for retreival on next sync
            $UsersToProvisionOnNextSync | Export-Clixml "$iPilotDataDirectory\UsersToProvisionFromLastSync_$($iPilotDomain).xml" -Force

        #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)"


                    $SetiPilotUserSplat = @{
                        UserPrincipalName = $iPilotUser.PreviousUPN
                        NewUserPrincipalName = $iPilotUser.userPrincipalName
                    }
                    Set-iPilotTeamsUser @SetiPilotUserSplat
                }
            } else {
                Write-Output "No users have UserPrincipalName changes"
            }

        #endregion Update users with UPN Changes

        #region Users removed from sync group

            $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 }
            #$UsersRemovedFromiPilotSyncGroup = $AlliPilotUsers | Where-Object {$UserPrincipalNamesRemovedFromiPilotSyncGroup -contains $_.userPrincipalName }

            # Add missing attributes
            $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)"

            $DeletedUsers += $UsersRemovedFromiPilotSyncGroup

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

            Foreach ($DeletedUser in $DeletedUsers) {

                # If phone number is present in AD and matches an assigned number in iPilot, auto deprovision user
                $DeletedUseriPilotBusinessPhone = Get-iPilotNumber -FilterByTelephoneNumber $DeletedUser.BusinessPhone
                if ($DeletedUseriPilotBusinessPhone) {
                    Write-Output "Deprovisioning $($DeletedUser.AzureADUserPrincipalName) - user was removed from $iPilotSyncGroupName"
                    Remove-iPilotTeamsUserAssignment -UserPrincipalName $DeletedUser.AzureADUserPrincipalName
                }

                # 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 -and !$DeletedUser.businessPhones) {
                    Write-Output "Deprovisioning $($DeletedUser.AzureADUserPrincipalName) - user was removed from $iPilotSyncGroupName and businessPhones is empty"
                    Remove-iPilotTeamsUserAssignment -UserPrincipalName $DeletedUser.AzureADUserPrincipalName
                } 

                # If phone number is present in AD and does NOT match number in iPilot, do nothing
                if ($DeletedUser.businessPhones -and !$DeletedUseriPilotBusinessPhone) {
                    Write-Verbose "Phone number ($($DeletedUser.businessPhones)) is present in Azure AD and does NOT match number in iPilot, do nothing"
                }

                # If phone number is not present in AD and does NOT match UPN in iPilot, do nothing
                if (!$DeletedUser.businessPhones -and !$DeletedUseriPilotUserPrincipalName) {
                    Write-Verbose "Phone number (businessPhones) is not present in Azure AD and does NOT match number in iPilot, do nothing"
                }
            }

        #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"
            } else {
                Write-Output "No users in $iPilotSyncGroupName have been disabled in Azure AD"
            }
            Foreach ($DisabledUser in $DisabledUsers) {
                Write-Output "Deprovisioning $($DisabledUser.AzureADUserPrincipalName) - user disable in Azure AD"
                Remove-iPilotTeamsUserAssignment -UserPrincipalName $DisabledUser.AzureADUserPrincipalName
            }

        #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

    #endregion End Logging
    
}