Public/UserManagement/VoiceConfig/Set-TeamsUserVoiceConfig.ps1

# Module: TeamsFunctions
# Function: VoiceConfig
# Author: David Eberhardt
# Updated: 01-DEC-2020
# Status: Live

#TODO Requirement capture for configuration for OperatorConnect needed
#TODO Add Location as a parameter here to assign with Set-CsPhoneNumberAssignment

function Set-TeamsUserVoiceConfig {
  <#
  .SYNOPSIS
    Enables a User to consume Voice services in Teams (Pstn breakout)
  .DESCRIPTION
    Enables a User for Direct Routing, Microsoft Callings or for use in Call Queues (EvOnly)
    User requires a Phone System License in any case.
  .PARAMETER UserPrincipalName
    Required. UserPrincipalName (UPN) of the User to change the configuration for
  .PARAMETER DirectRouting
    Optional (Default Parameter Set). Limits the Scope to enable an Object for DirectRouting
  .PARAMETER CallingPlans
    Required for CallingPlans. Limits the Scope to enable an Object for CallingPlans
  .PARAMETER PhoneNumber
    Optional. Phone Number in E.164 format to be assigned to the User.
    For proper configuration a PhoneNumber is required. Without it, the User will not be able to make or receive calls.
    This script does not enforce all Parameters and is intended to validate and configure one or all Parameters.
    For enforced ParameterSet please call New-TeamsUserVoiceConfig
    For DirectRouting, will populate the LineUri
    For CallingPlans, will populate the TelephoneNumber (must be present in the Tenant)
  .PARAMETER OnlineVoiceRoutingPolicy
    Optional. Required for DirectRouting. Assigns an Online Voice Routing Policy to the User
  .PARAMETER TenantDialPlan
    Optional. Optional for DirectRouting. Assigns a Tenant Dial Plan to the User
  .PARAMETER CallingLineIdentity
    Optional. Assigns a Calling Line Identity to the User
  .PARAMETER CallingPlanLicense
    Optional. Optional for CallingPlans. Assigns a Calling Plan License to the User.
    Must be one of the set: InternationalCallingPlan DomesticCallingPlan DomesticCallingPlan120 CommunicationCredits DomesticCallingPlan120b
  .PARAMETER PassThru
    Optional. Displays Object after action.
  .PARAMETER Force
    By default, this script only applies changed elements. Force overwrites configuration regardless of current status.
    Additionally Suppresses confirmation inputs except when $Confirm is explicitly specified
  .PARAMETER WriteErrorLog
    If Errors are encountered, writes log to C:\Temp
  .EXAMPLE
    Set-TeamsUserVoiceConfig -UserPrincipalName John@domain.com -CallingPlans -PhoneNumber "+15551234567" -CallingPlanLicense DomesticCallingPlan
 
    Provisions John@domain.com for Calling Plans with the Calling Plan License and Phone Number provided
  .EXAMPLE
    Set-TeamsUserVoiceConfig -UserPrincipalName John@domain.com -CallingPlans -PhoneNumber "+15551234567" -WriteErrorLog
 
    Provisions John@domain.com for Calling Plans with the Phone Number provided (requires Calling Plan License to be assigned already)
    If Errors are encountered, they are written to C:\Temp as well as on screen
  .EXAMPLE
    Set-TeamsUserVoiceConfig -UserPrincipalName John@domain.com -DirectRouting -PhoneNumber "+15551234567" -OnlineVoiceRoutingPolicy "O_VP_AMER"
 
    Provisions John@domain.com for DirectRouting with the Online Voice Routing Policy and Phone Number provided
  .EXAMPLE
    Set-TeamsUserVoiceConfig -UserPrincipalName John@domain.com -PhoneNumber "+15551234567" -OnlineVoiceRoutingPolicy "O_VP_AMER" -TenantDialPlan "DP-US"
 
    Provisions John@domain.com for DirectRouting with the Online Voice Routing Policy, Tenant Dial Plan and Phone Number provided
  .EXAMPLE
    Set-TeamsUserVoiceConfig -UserPrincipalName John@domain.com -PhoneNumber "+15551234567" -OnlineVoiceRoutingPolicy "O_VP_AMER"
 
    Provisions John@domain.com for DirectRouting with the Online Voice Routing Policy and Phone Number provided.
  .EXAMPLE
    Set-TeamsUserVoiceConfig -UserPrincipalName John@domain.com -PhoneNumber "+15551234567" -CallingLineIdentity "CLI-15551234000"
 
    Provisions John@domain.com for DirectRouting with the Calling Line Identity and Phone Number provided.
  .INPUTS
    System.String
  .OUTPUTS
    System.Void - Default Behaviour
    System.Object - With Switch PassThru
    System.File - With Switch WriteErrorLog
  .NOTES
    ParameterSet 'DirectRouting' will provision a User to use DirectRouting. Enables User for Enterprise Voice,
    assigns a Number and an Online Voice Routing Policy and optionally also a Tenant Dial Plan. This is the default.
    ParameterSet 'CallingPlans' will provision a User to use Microsoft CallingPlans.
    Enables User for Enterprise Voice and assigns a Microsoft Number (must be found in the Tenant!)
    Optionally can also assign a Calling Plan license prior.
    This script cannot apply PhoneNumbers for OperatorConnect yet
    This script accepts pipeline input as Value (UserPrincipalName) or as Object (UPN, OVP, TDP, PhoneNumber)
    This enables bulk provisioning
  .COMPONENT
    VoiceConfiguration
  .FUNCTIONALITY
    Applying Voice Configuration parameters to a User
  .LINK
    https://github.com/DEberhardt/TeamsFunctions/tree/main/docs/Set-TeamsUserVoiceConfig.md
  .LINK
    https://github.com/DEberhardt/TeamsFunctions/tree/main/docs/about_VoiceConfiguration.md
  .LINK
    https://github.com/DEberhardt/TeamsFunctions/tree/main/docs/
  #>


  [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidGlobalVars', '', Justification = 'Required for performance. Removed with Disconnect-Me')]
  [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'DirectRouting', ConfirmImpact = 'Medium')]
  [Alias('Set-TeamsUVC')]
  [OutputType([System.Object])]
  param(
    [Parameter(Mandatory, Position = 0, ValueFromPipelineByPropertyName, ValueFromPipeline, HelpMessage = 'UserPrincipalName of the User')]
    [Alias('ObjectId', 'Identity')]
    [string]$UserPrincipalName,

    [Parameter(ParameterSetName = 'DirectRouting', HelpMessage = 'Enables an Object for Direct Routing')]
    [switch]$DirectRouting,

    [Parameter(ParameterSetName = 'DirectRouting', ValueFromPipelineByPropertyName, HelpMessage = 'Name of the Online Voice Routing Policy')]
    [AllowNull()]
    [AllowEmptyString()]
    [Alias('OVP')]
    [ValidateScript( {
        if ($null -eq $_ -or $_ -eq '' -or $_ -in $(&$global:TfAcSbVoiceRoutingPolicy)) { return $true } else {
          throw [System.Management.Automation.ValidationMetadataException] 'Value must be a valid Policy in the Tenant. Use Intellisense for options'
        } })]
    [ArgumentCompleter({ &$global:TfAcSbVoiceRoutingPolicy })]
    [string]$OnlineVoiceRoutingPolicy,

    [Parameter(ValueFromPipelineByPropertyName, HelpMessage = 'Name of the Tenant Dial Plan')]
    [AllowNull()]
    [AllowEmptyString()]
    [Alias('TDP')]
    [ValidateScript( {
        if ($null -eq $_ -or $_ -eq '' -or $_ -in $(&$global:TfAcSbTenantDialPlan)) { return $true } else {
          throw [System.Management.Automation.ValidationMetadataException] 'Value must be a valid Dial Plan in the Tenant. Use Intellisense for options'
        } })]
    [ArgumentCompleter({ &$global:TfAcSbTenantDialPlan })]
    [string]$TenantDialPlan,

    [Parameter(ValueFromPipelineByPropertyName, HelpMessage = 'Name of the Tenant Dial Plan')]
    [AllowNull()]
    [AllowEmptyString()]
    [Alias('CLI')]
    [ValidateScript( {
        if ($null -eq $_ -or $_ -eq '' -or $_ -in $(&$global:TfAcSbTeamsCallingLineIdentity)) { return $true } else {
          throw [System.Management.Automation.ValidationMetadataException] 'Value must be a valid Policy in the Tenant. Use Intellisense for options'
        } })]
    [ArgumentCompleter({ &$global:TfAcSbTeamsCallingLineIdentity })]
    [string]$CallingLineIdentity,

    [Parameter(ValueFromPipelineByPropertyName, HelpMessage = 'E.164 Number to assign to the Object')]
    [AllowNull()]
    [AllowEmptyString()]
    [Alias('Number', 'LineURI')]
    [string]$PhoneNumber,

    [Parameter(ParameterSetName = 'CallingPlans', Mandatory, HelpMessage = 'Enables an Object for Microsoft Calling Plans')]
    [switch]$CallingPlan,

    [Parameter(ParameterSetName = 'CallingPlans', HelpMessage = 'Calling Plan License to assign to the Object')]
    [ValidateScript( {
        if ($_ -in $(&$global:TfAcSbCallingPlanLicense)) { return $true } else {
          throw [System.Management.Automation.ValidationMetadataException] 'Value must be a valid Calling Plan. Use Intellisense for options or Get-AzureAdLicense (ParameterName)'
        } })]
    [ArgumentCompleter({ &$global:TfAcSbCallingPlanLicense })]
    [string[]]$CallingPlanLicense,

    [Parameter(HelpMessage = 'Suppresses confirmation prompt unless -Confirm is used explicitly')]
    [switch]$Force,

    [Parameter(HelpMessage = 'No output is written by default, Switch PassThru will return changed object')]
    [switch]$PassThru,

    [Parameter(HelpMessage = 'Writes a Log File of Errors to C:\Temp')]
    [switch]$WriteErrorLog
  ) #param

  begin {
    #break
    Show-FunctionStatus -Level Live
    Write-Verbose -Message "[BEGIN ] $($MyInvocation.MyCommand)"

    # Asserting AzureAD Connection
    if ( -not $script:TFPSSA) { $script:TFPSSA = Assert-AzureADConnection; if ( -not $script:TFPSSA ) { break } }

    # Asserting MicrosoftTeams Connection
    if ( -not (Assert-MicrosoftTeamsConnection) ) { break }

    # Setting Preference Variables according to Upstream settings
    if (-not $PSBoundParameters.ContainsKey('Verbose')) { $VerbosePreference = $PSCmdlet.SessionState.PSVariable.GetValue('VerbosePreference') }
    if (-not $PSBoundParameters.ContainsKey('Confirm')) { $ConfirmPreference = $PSCmdlet.SessionState.PSVariable.GetValue('ConfirmPreference') }
    if (-not $PSBoundParameters.ContainsKey('WhatIf')) { $WhatIfPreference = $PSCmdlet.SessionState.PSVariable.GetValue('WhatIfPreference') }
    if (-not $PSBoundParameters.ContainsKey('Debug')) { $DebugPreference = $PSCmdlet.SessionState.PSVariable.GetValue('DebugPreference') } else { $DebugPreference = 'Continue' }
    if ( $PSBoundParameters.ContainsKey('InformationAction')) { $InformationPreference = $PSCmdlet.SessionState.PSVariable.GetValue('InformationAction') } else { $InformationPreference = 'Continue' }

    #Initialising Counters
    $private:StepsID0, $private:StepsID1 = Get-WriteBetterProgressSteps -Code $($MyInvocation.MyCommand.Definition) -MaxId 1
    $private:ActivityID0 = $($MyInvocation.MyCommand.Name)
    [int] $private:CountID0 = [int] $private:CountID1 = 1

    $Stack = Get-PSCallStack
    $Called = ($stack.length -ge 3)

    $RemoveTDP = ( $PSBoundParameters.ContainsKey('TenantDialPlan') -and $null -eq $TenantDialPlan )
    $RemoveCLI = ( $PSBoundParameters.ContainsKey('CallingLineIdentity') -and $null -eq $CallingLineIdentity )
    $RemoveOVP = ( $PSBoundParameters.ContainsKey('OnlineVoiceRoutingPolicy') -and $null -eq $OnlineVoiceRoutingPolicy )

  } #begin

  process {
    Write-Verbose -Message "[PROCESS] $($MyInvocation.MyCommand)"
    # Initialising $ErrorLog
    [System.Collections.Generic.List[object]]$ErrorLog = @()

    $ActivityID0 = "Processing '$UserPrincipalName'"
    $StatusID0 = 'Information Gathering'
    #region Querying Identity
    try {
      $CurrentOperationID0 = 'Querying User Account (TeamsUserVoiceConfig)'
      Write-BetterProgress -Id 0 -Activity $ActivityID0 -Status $StatusID0 -CurrentOperation $CurrentOperationID0 -Step ($private:CountID0++) -Of $private:StepsID0
      $CsUser = Get-CsOnlineUser -Identity "$UserPrincipalName" -InformationAction SilentlyContinue -WarningAction SilentlyContinue -ErrorAction Stop
      $ObjectType = switch -regex ($CsUser.InterpretedUserType) {
        'ApplicationInstance' { 'ResourceAccount' }
        'User' { 'User' }
        default { 'Unknown' }
      }

      $CurrentOperationID0 = 'Querying User Account (AzureAdUserLicense)'
      Write-BetterProgress -Id 0 -Activity $ActivityID0 -Status $StatusID0 -CurrentOperation $CurrentOperationID0 -Step ($private:CountID0++) -Of $private:StepsID0
      $UserLicense = Get-AzureAdUserLicense -UserPrincipalName "$($CsUser.UserPrincipalName)" -InformationAction SilentlyContinue -WarningAction SilentlyContinue -ErrorAction Stop
      $IsPSsuccess = $UserLicense.PhoneSystemStatus.Contains('Success')
    }
    catch {
      Write-Error "'$UserPrincipalName' - Object not found: $($_.Exception.Message)" -Category ObjectNotFound
      $ErrorLog += $_.Exception.Message
      if ( $WriteErrorLog.IsPresent ) { Write-TFErrorLog -ErrorLog $ErrorLog -Artifact $($UserPrincipalName) }
      return
    }
    #endregion

    $StatusID0 = 'Establishing User Object Readiness'
    #region Establishing User Object Readiness
    $Operation = 'Asserting Callable Entity'
    if ( -not $IsPSsuccess) {
      $CurrentOperationID0 = "$Operation`: Asserting Callable Entity"
      Write-BetterProgress -Id 0 -Activity $ActivityID0 -Status $StatusID0 -CurrentOperation $CurrentOperationID0 -Step ($private:CountID0++) -Of $private:StepsID0
      try {
        $Assertion = $null
        $Assertion = Assert-TeamsCallableEntity -UserObject $CsUser -LicenseObject $UserLicense -InformationAction SilentlyContinue -WarningAction SilentlyContinue -ErrorAction Stop
        if ($Assertion) {
          Write-Information "SUCCESS: '$($CsUser.UserPrincipalName)' - PhoneSystem License & Status: OK"
        }
        else {
          throw "$($Error[0].Exception.Message)"
        }
      }
      catch {
        Write-Error "'$($CsUser.UserPrincipalName)' - $Operation`: Error encountered when asserting Entity: $($_.Exception.Message)" -Category ObjectNotFound
        $ErrorLog += $_.Exception.Message
        if ( $WriteErrorLog.IsPresent ) { Write-TFErrorLog -ErrorLog $ErrorLog -Artifact $($CsUser.UserPrincipalName) }
        return
      }
    }
    else {
      Write-Verbose -Message "'$($CsUser.UserPrincipalName)' - PhoneSystem License & Status: OK" -Verbose
    }
    #endregion

    #region Checking multiple assignments of PhoneSystem
    $CurrentOperationID0 = 'Checking multiple assignments of PhoneSystem'
    Write-BetterProgress -Id 0 -Activity $ActivityID0 -Status $StatusID0 -CurrentOperation $CurrentOperationID0 -Step ($private:CountID0++) -Of $private:StepsID0
    if ( $UserLicense.PhoneSystemStatus.Contains(',')) {
      Write-Warning -Message "'$($CsUser.UserPrincipalName)' - $Operation`: Multiple assignments found. Please verify License assignment."
      Write-Verbose -Message 'All licenses assigned to the User:' -Verbose
      Write-Output $UserLicense.Licenses | Select-Object ProductName, SkuPartNumber, LicenseType, IncludesTeams, IncludesPhoneSystem, ServicePlans
    }
    #endregion

    #region Calling Plans - Number verification
    if ( $PSCmdlet.ParameterSetName -eq 'CallingPlans' ) {
      $CurrentOperationID0 = 'Testing Object for Calling Plan License'
      Write-BetterProgress -Id 0 -Activity $ActivityID0 -Status $StatusID0 -CurrentOperation $CurrentOperationID0 -Step ($private:CountID0++) -Of $private:StepsID0
      # Validating License assignment
      try {
        if ( -not $CallingPlanLicense ) {
          if ( -not ($Userlicense.licenses | Where-Object LicenseType -EQ 'CallingPlan') ) {
            # This could be done with Test-TeamsUserHasCallingPlan
            Write-Progress -Id 0 -Activity $ActivityID0 -Completed
            throw "'$($CsUser.UserPrincipalName)' - User is not licensed correctly. Please check License assignment. A Calling Plan License is required"
          }
        }
      }
      catch {
        # Unlicensed
        Write-Progress -Id 0 -Activity $ActivityID0 -Completed
        $ErrorLogMessage = 'User is not licensed (CallingPlan). Please assign a Calling Plan license'
        Write-Error -Message $ErrorLogMessage -Category ResourceUnavailable -RecommendedAction 'Please assign a Calling Plan license' -ErrorAction Stop
        $ErrorLog += $ErrorLogMessage
        $ErrorLog += $_.Exception.Message
        if ( $WriteErrorLog.IsPresent ) { Write-TFErrorLog -ErrorLog $ErrorLog -Artifact $($CsUser.UserPrincipalName) }
        return
      }
    }
    #endregion

    #region Validating Phone Number Format
    $Operation = 'Phone Number'
    $CurrentOperationID0 = "$Operation`: Querying current assignment"
    Write-BetterProgress -Id 0 -Activity $ActivityID0 -Status $StatusID0 -CurrentOperation $CurrentOperationID0 -Step ($private:CountID0++) -Of $private:StepsID0
    # Querying CurrentPhoneNumber
    try {
      $CurrentPhoneNumber = $CsUser.LineUri
      Write-Verbose -Message "'$($CsUser.UserPrincipalName)' - $Operation`: Currently assigned: $CurrentPhoneNumber"
    }
    catch {
      $CurrentPhoneNumber = $null
      Write-Verbose -Message "'$($CsUser.UserPrincipalName)' - $Operation`: Currently assigned: NONE"
    }

    if ($PSBoundParameters.ContainsKey('PhoneNumber')) {
      $CurrentOperationID0 = "$Operation`: Validating format"
      Write-BetterProgress -Id 0 -Activity $ActivityID0 -Status $StatusID0 -CurrentOperation $CurrentOperationID0 -Step ($private:CountID0++) -Of $private:StepsID0
      if ( [String]::IsNullOrEmpty($PhoneNumber) ) {
        if ($CurrentPhoneNumber) {
          Write-Verbose -Message "'$($CsUser.UserPrincipalName)' - $Operation`: Number is NULL or Empty. The Existing Number '$CurrentPhoneNumber' will be removed"
        }
        else {
          Write-Verbose -Message "'$($CsUser.UserPrincipalName)' - $Operation`: Number is NULL or Empty, but no Number is currently assigned. No Action taken"
        }
        $PhoneNumber = $null
      }
      else {
        If ($PhoneNumber -notmatch '^(tel:\+|\+)?([0-9]?[-\s]?(\(?[0-9]{3}\)?)[-\s]?([0-9]{3}[-\s]?[0-9]{4})|[0-9]{8,15})((;ext=)([0-9]{3,8}))?$') {
          Write-Error -Message "'$($CsUser.UserPrincipalName)' - $Operation`: '$PhoneNumber' is not in an acceptable format. Multiple formats are available, but preferred is E.164 or LineURI format, with a minimum of 8 digits." -Category InvalidFormat
        }
        else {
          Write-Verbose -Message "'$($CsUser.UserPrincipalName)' - $Operation`: PhoneNumber '$PhoneNumber' will be applied"
        }

        if ( $Force ) {
          # Ascertaining impact on removal
          $E164Number = Format-StringForUse $PhoneNumber -As E164
          $UserWithThisNumber = Get-TeamsPhoneNumber -PhoneNumber "$E164Number" -InformationAction SilentlyContinue | Where-Object Assigned
          $UserWithThisNumberIsSelf = $UserWithThisNumber | Where-Object { $_.AssignedTo -EQ $($CsUser.UserPrincipalName) -or $_.AssignedToSIP -EQ $($CsUser.UserPrincipalName) }
          $UserWithThisNumberExceptSelf = $UserWithThisNumber | Where-Object { -not ($_.AssignedTo -EQ $($CsUser.UserPrincipalName) -or $_.AssignedToSIP -EQ $($CsUser.UserPrincipalName)) }
          if ( $UserWithThisNumberIsSelf ) {
            if ($Force) {
              Write-Verbose -Message "'$($CsUser.UserPrincipalName)' - $Operation`: Assigned to self, will be reapplied"
            }
            else {
              Write-Verbose -Message "'$($CsUser.UserPrincipalName)' - $Operation`: Assigned to self, no action taken"
            }
          }
          if ( $UserWithThisNumberExceptSelf ) {
            if ($Force) {
              Write-Warning -Message "'$($CsUser.UserPrincipalName)' - $Operation`: '$($CsUser.LineUri)' is currently assigned to Object(s): $($UserWithThisNumberExceptSelf.AssignedTo -join ','). This assignment will be removed!"
            }
            else {
              Write-Error -Message "'$($CsUser.UserPrincipalName)' - $Operation`: '$($CsUser.LineUri)' is already assigned to other Object(s): $($UserWithThisNumberExceptSelf.AssignedTo -join ',')" -Category NotImplemented -RecommendedAction 'Please specify a different Number or use -Force to re-assign' -ErrorAction Stop
            }
          }
        }
      }
    }
    else {
      #PhoneNumber is not provided
      if ( -not $CurrentPhoneNumber ) {
        Write-Warning -Message "'$($CsUser.UserPrincipalName)' - $Operation`: Not provided or present. User will not be able to use PhoneSystem"
      }
    }
    #endregion
    #endregion

    $StatusID0 = 'Applying Voice Configuration'
    #region Apply Voice Config
    if ($Force -or $PSCmdlet.ShouldProcess("$($CsUser.UserPrincipalName)", 'Apply Voice Configuration')) {

      #region Calling Line Identity
      $Operation = 'Calling Line Identity'
      if ( $PSBoundParameters.ContainsKey('CallingLineIdentity') ) {
        if ( $Force -or $RemoveCLI ) {
          $CurrentOperationID0 = "Removing $Operation"
          Write-BetterProgress -Id 0 -Activity $ActivityID0 -Status $StatusID0 -CurrentOperation $CurrentOperationID0 -Step ($private:CountID0++) -Of $private:StepsID0
          try {
            Grant-CsCallingLineIdentity -Identity "$($CsUser.UserPrincipalName)" -PolicyName $null -ErrorAction Stop
            Write-Information "SUCCESS: '$($CsUser.UserPrincipalName)' - $Operation`: OK - Removed"
          }
          catch {
            $ErrorLogMessage = "'$($CsUser.UserPrincipalName)' - $Operation`: Failed: $($_.Exception.Message)"
            Write-Error -Message $ErrorLogMessage
            $ErrorLog += $ErrorLogMessage
          }
        }
        if ( $CallingLineIdentity ) {
          if ( $Force -or ([string]$CsUser.CallingLineIdentity -ne $CallingLineIdentity) ) {
            $CurrentOperationID0 = "Applying $Operation"
            Write-BetterProgress -Id 0 -Activity $ActivityID0 -Status $StatusID0 -CurrentOperation $CurrentOperationID0 -Step ($private:CountID0++) -Of $private:StepsID0
            try {
              if ( $ObjectType -eq 'User' ) {
                Grant-CsCallingLineIdentity -Identity "$($CsUser.UserPrincipalName)" -PolicyName $CallingLineIdentity -ErrorAction Stop
                Write-Information "SUCCESS: '$($CsUser.UserPrincipalName)' - $Operation`: OK - '$CallingLineIdentity'"
              }
              else {
                Write-Verbose -Message "'$($CsUser.UserPrincipalName)' - $Operation`: Operation not available for ObjectType '$ObjectType'" -Verbose
              }
            }
            catch {
              $ErrorLogMessage = "'$($CsUser.UserPrincipalName)' - $Operation`: Failed: $($_.Exception.Message)"
              Write-Error -Message $ErrorLogMessage
              $ErrorLog += $ErrorLogMessage
            }
          }
          else {
            Write-Verbose -Message "'$($CsUser.UserPrincipalName)' - $Operation`: '$([string]$CsUser.CallingLineIdentity)' present" -Verbose
          }
        }
      }
      else {
        if ($CsUser.CallingLineIdentity) {
          $StatusMessage = "'$($CsUser.CallingLineIdentity)' present (not in scope)"
        }
        else {
          $StatusMessage = 'Not provided (not in scope)'
        }
        Write-Verbose -Message "'$($CsUser.UserPrincipalName)' - $Operation`: $StatusMessage"
      }
      #endregion


      #region Tenant Dial Plan
      $Operation = 'Tenant Dial Plan'
      if ( $PSBoundParameters.ContainsKey('TenantDialPlan') ) {
        if ( $Force -or $RemoveTDP ) {
          $CurrentOperationID0 = "Removing $Operation"
          Write-BetterProgress -Id 0 -Activity $ActivityID0 -Status $StatusID0 -CurrentOperation $CurrentOperationID0 -Step ($private:CountID0++) -Of $private:StepsID0
          try {
            Grant-CsTenantDialPlan -Identity "$($CsUser.UserPrincipalName)" -PolicyName $null -ErrorAction Stop
            Write-Information "SUCCESS: '$($CsUser.UserPrincipalName)' - $Operation`: OK - Removed"
          }
          catch {
            $ErrorLogMessage = "'$($CsUser.UserPrincipalName)' - $Operation`: Failed: $($_.Exception.Message)"
            Write-Error -Message $ErrorLogMessage
            $ErrorLog += $ErrorLogMessage
          }
        }
        if ( $TenantDialPlan ) {
          if ( $Force -or ([string]$CsUser.TenantDialPlan -ne $TenantDialPlan) ) {
            $CurrentOperationID0 = "Applying $Operation"
            Write-BetterProgress -Id 0 -Activity $ActivityID0 -Status $StatusID0 -CurrentOperation $CurrentOperationID0 -Step ($private:CountID0++) -Of $private:StepsID0
            try {
              if ( $ObjectType -eq 'User' ) {
                Grant-CsTenantDialPlan -Identity "$($CsUser.UserPrincipalName)" -PolicyName $TenantDialPlan -ErrorAction Stop
                Write-Information "SUCCESS: '$($CsUser.UserPrincipalName)' - $Operation`: OK - '$TenantDialPlan'"
              }
              else {
                Write-Verbose -Message "'$($CsUser.UserPrincipalName)' - $Operation`: Operation not available for ObjectType '$ObjectType'" -Verbose
              }
            }
            catch {
              $ErrorLogMessage = "'$($CsUser.UserPrincipalName)' - $Operation`: Failed: $($_.Exception.Message)"
              Write-Error -Message $ErrorLogMessage
              $ErrorLog += $ErrorLogMessage
            }
          }
          else {
            Write-Verbose -Message "'$($CsUser.UserPrincipalName)' - $Operation`: '$([string]$CsUser.TenantDialPlan)' present" -Verbose
          }
        }
      }
      else {
        if ($CsUser.TenantDialPlan) {
          $StatusMessage = "'$($CsUser.TenantDialPlan)' present (not in scope)"
        }
        else {
          $StatusMessage = 'Not provided (not in scope)'
        }
        Write-Verbose -Message "'$($CsUser.UserPrincipalName)' - $Operation`: $StatusMessage" -Verbose
      }
      #endregion


      #region Specific Configuration 1 - OVP or Calling Plan License
      switch ($PSCmdlet.ParameterSetName) {
        'DirectRouting' {
          $StatusID0 = 'Applying Voice Configuration: Provisioning for Direct Routing'
          # Apply $OnlineVoiceRoutingPolicy
          $Operation = 'Online Voice Routing Policy'
          if ( $PSBoundParameters.ContainsKey('OnlineVoiceRoutingPolicy') ) {
            if ( $Force -or $RemoveOVP ) {
              $CurrentOperationID0 = "Removing $Operation"
              Write-BetterProgress -Id 0 -Activity $ActivityID0 -Status $StatusID0 -CurrentOperation $CurrentOperationID0 -Step ($private:CountID0++) -Of $private:StepsID0
              try {
                Grant-CsOnlineVoiceRoutingPolicy -Identity "$($CsUser.UserPrincipalName)" -PolicyName $null -ErrorAction Stop
                Write-Information "SUCCESS: '$($CsUser.UserPrincipalName)' - $Operation`: OK - Removed"
              }
              catch {
                $ErrorLogMessage = "'$($CsUser.UserPrincipalName)' - $Operation`: Failed: $($_.Exception.Message)"
                Write-Error -Message $ErrorLogMessage
                $ErrorLog += $ErrorLogMessage
              }
            }
            if ( $OnlineVoiceRoutingPolicy ) {
              if ( $Force -or ([string]$CsUser.OnlineVoiceRoutingPolicy -ne $OnlineVoiceRoutingPolicy) ) {
                $CurrentOperationID0 = "Applying $Operation"
                Write-BetterProgress -Id 0 -Activity $ActivityID0 -Status $StatusID0 -CurrentOperation $CurrentOperationID0 -Step ($private:CountID0++) -Of $private:StepsID0
                try {
                  Grant-CsOnlineVoiceRoutingPolicy -Identity "$($CsUser.UserPrincipalName)" -PolicyName $OnlineVoiceRoutingPolicy -ErrorAction Stop
                  Write-Information "SUCCESS: '$($CsUser.UserPrincipalName)' - $Operation`: OK - '$OnlineVoiceRoutingPolicy'"
                }
                catch {
                  $ErrorLogMessage = "'$($CsUser.UserPrincipalName)' - $Operation`: Failed: $($_.Exception.Message)"
                  Write-Error -Message $ErrorLogMessage
                  $ErrorLog += $ErrorLogMessage
                }
              }
              else {
                Write-Verbose -Message "'$($CsUser.UserPrincipalName)' - $Operation`: '$([string]$CsUser.OnlineVoiceRoutingPolicy)' present" -Verbose
              }
            }
          }
          else {
            if ( $CsUser.OnlineVoiceRoutingPolicy ) {
              $StatusMessage = "'$($CsUser.OnlineVoiceRoutingPolicy)' present (not in scope)"
              Write-Verbose -Message "'$($CsUser.UserPrincipalName)' - $Operation`: $StatusMessage" -Verbose
            }
            else {
              $StatusMessage = 'Not provided (not in scope)'
              Write-Verbose -Message "'$($CsUser.UserPrincipalName)' - $Operation`: $StatusMessage" -Verbose
              Write-Warning -Message "'$($CsUser.UserPrincipalName)' - $Operation`: Object will be able to receive inbound calls, but not make outbound calls!'"
              if ( $ObjectType -eq 'ResourceAccount' ) {
                Write-Verbose -Message 'Resource Accounts only require an Online Voice Routing Policy if the associated Call Queue or Auto Attendant forwards to PSTN' -Verbose
              }
            }
          }
        }
        'OperatorConnect' {
          $StatusID0 = 'Applying Voice Configuration: Provisioning for Operator Connect'
          # OperatorConnect - Requirement capture needed
          <#
          $CurrentOperationID0 = 'Applying Voice Configuration: Operator Connect'
          Write-BetterProgress -Id 0 -Activity $ActivityID0 -Status $StatusID0 -CurrentOperation $CurrentOperationID0 -Step ($private:CountID0++) -Of $private:StepsID0
          #>

        }
        'CallingPlans' {
          $StatusID0 = 'Applying Voice Configuration: Provisioning for Calling Plans'
          # Apply $CallingPlanLicense
          $Operation = 'Calling Plan License'
          $CurrentOperationID0 = "Applying $Operation"
          Write-BetterProgress -Id 0 -Activity $ActivityID0 -Status $StatusID0 -CurrentOperation $CurrentOperationID0 -Step ($private:CountID0++) -Of $private:StepsID0
          if ($CallingPlanLicense) {
            try {
              $null = (Set-TeamsUserLicense -Identity "$($CsUser.UserPrincipalName)" -Add $CallingPlanLicense -ErrorAction STOP)
              Write-Information "SUCCESS: '$($CsUser.UserPrincipalName)' - $Operation`: OK - '$CallingPlanLicense'"
            }
            catch {
              $ErrorLogMessage = "'$($CsUser.UserPrincipalName)' - $Operation`: Failed for '$CallingPlanLicense' with Exception: '$($_.Exception.Message)'"
              Write-Error -Message $ErrorLogMessage
              $ErrorLog += $ErrorLogMessage
            }
            #VALIDATE Waiting period after applying a Calling Plan license? Will Phone Number assignment succeed right away?
            Write-Verbose -Message 'Calling Plan License has been applied, but replication time has not been factored in or tested. Applying a Phone Number may fail. If so, please run command again after a few minutes and feed back duration to TeamsFunctions@outlook.com or via GitHub!' -Verbose
          }
        }
      }
      #endregion

      #region Specific Configuration 2 - Phone Number
      $StatusID0 = 'Applying Voice Configuration: Phone Number'
      #BODGE Reinstated as Set-TeamsPhoneNumber scavanging did not execute properly - investigate
      #<# Removed as this is performed by Set-TeamsPhoneNumber, can be re-instated any time.
      #region Removing number from OTHER Object
      $Operation = $CurrentOperationID0 = 'Scavenging Phone Number'
      if ( $Force -and $PSBoundParameters.ContainsKey('PhoneNumber') -and $UserWithThisNumberExceptSelf ) {
        Write-BetterProgress -Id 0 -Activity $ActivityID0 -Status $StatusID0 -CurrentOperation $CurrentOperationID0 -Step ($private:CountID0++) -Of $private:StepsID0
        Write-Warning -Message 'Parameter Force - Scavenging Phone Number from all Objects where number is assigned. Validate carefully'
        foreach ($UserWTN in $UserWithThisNumberExceptSelf) {
          try {
            Write-Verbose -Message "'$($CsUser.UserPrincipalName)' - $Operation FROM '$($UserWTN.AssignedTo)'"
            # Call to Set-TeamsPhoneNumber as we are scavenging (no information available for these numbers)
            $SetTeamsPhoneNumberParams = $null
            $SetTeamsPhoneNumberParams = @{
              'UserPrincipalName' = $UserWTN.AssignedTo
              'PhoneNumber'       = $null
              'WarningAction'     = 'SilentlyContinue'
              'ErrorAction'       = 'Stop'
              'Force'             = $Force
            }
            if ($PSBoundParameters.ContainsKey('Debug') -or $DebugPreference -eq 'Continue') {
              "Function: $($MyInvocation.MyCommand.Name) - Parameters (Set-TeamsPhoneNumber)", ($SetTeamsPhoneNumberParams | Format-Table -AutoSize | Out-String).Trim() | Write-Debug
            }
            Set-TeamsPhoneNumber @SetTeamsPhoneNumberParams
            $StatusMessage = "$($UserWTN.NumberType) Number removed from $($UserWTN.AssignedToObjectType) '$($UserWTN.AssignedTo)': '$($UserWTN.PhoneNumber)'"
            Write-Information "SUCCESS: '$($CsUser.UserPrincipalName)' - $Operation`: OK - $StatusMessage"
          }
          catch {
            $ErrorLogMessage = "'$($CsUser.UserPrincipalName)' - $Operation`: Failed: $($_.Exception.Message)"
            $ErrorLog += $ErrorLogMessage
            Write-Information "FAILED: '$($CsUser.UserPrincipalName)' - $Operation`: Failed to execute for '$($CsUser.LineURI)'"
            Write-Error -Message $ErrorLogMessage -ErrorAction $ErrorActionPreference
          }
        }
      }
      #endregion
      #>

      $Operation = $CurrentOperationID0 = 'Removing Phone Number'
      if ( $PSBoundParameters.ContainsKey('PhoneNumber')) {
        #region Remove Number from current Object
        if ( $Force -or ([String]::IsNullOrEmpty($PhoneNumber)) ) {
          Write-BetterProgress -Id 0 -Activity $ActivityID0 -Status $StatusID0 -CurrentOperation $CurrentOperationID0 -Step ($private:CountID0++) -Of $private:StepsID0
          if ( ([String]::IsNullOrEmpty($PhoneNumber)) ) {
            Write-Warning -Message "'$($CsUser.UserPrincipalName)' - PhoneNumber is empty and will be removed. The User will not be able to use PhoneSystem!"
          }
          try {
            # Call to Set-TFPhoneNumber as we have all information and can pass it on.
            $SetTFPhoneNumberParams = $null
            $SetTFPhoneNumberParams = @{
              'UserObject'    = $CsUser
              'UserLicense'   = $UserLicense
              'PhoneNumber'   = $null
              'Called'        = $Called
              'Force'         = $Force
              'WarningAction' = 'SilentlyContinue'
              'ErrorAction'   = 'Stop'
            }
            if ($PSBoundParameters.ContainsKey('Debug') -or $DebugPreference -eq 'Continue') {
              "Function: $($MyInvocation.MyCommand.Name) - Parameters (Set-TFPhoneNumber)", ($SetTFPhoneNumberParams | Format-Table -AutoSize | Out-String).Trim() | Write-Debug
            }
            Set-TFPhoneNumber @SetTFPhoneNumberParams
            $StatusMessage = "Number removed from $ObjectType`: '$($CsUser.LineUri)'"
            Write-Information "SUCCESS: '$($CsUser.UserPrincipalName)' - $Operation`: OK - $StatusMessage"
          }
          catch {
            $ErrorLogMessage = "'$($CsUser.UserPrincipalName)' - $Operation`: Failed: $($_.Exception.Message)"
            $ErrorLog += $ErrorLogMessage
            Write-Information "FAILED: '$($CsUser.UserPrincipalName)' - $Operation`: Failed to execute"
            Write-Error -Message $ErrorLogMessage -ErrorAction $ErrorActionPreference
          }
        }
        #endregion

        #region Applying Phone Number
        $Operation = $CurrentOperationID0 = 'Applying Phone Number'
        if ( -not [String]::IsNullOrEmpty($PhoneNumber) ) {
          Write-BetterProgress -Id 0 -Activity $ActivityID0 -Status $StatusID0 -CurrentOperation $CurrentOperationID0 -Step ($private:CountID0++) -Of $private:StepsID0
          #TODO Validate normalisation of Number before comparison - may not be needed and reports that it takes some time!
          if ( $Force -or ($CurrentPhoneNumber -ne ($PhoneNumber | Format-StringForUse -As LineUri)) ) {
            # Phone Number is different and needs to be applied or Force is used
            try {
              # Call to Set-TFPhoneNumber as we have all information and can pass it on.
              $SetTFPhoneNumberParams = $null
              $SetTFPhoneNumberParams = @{
                'UserObject'    = $CsUser
                'UserLicense'   = $UserLicense
                'PhoneNumber'   = $PhoneNumber
                'Called'        = $Called
                'Force'         = $Force
                'WarningAction' = 'SilentlyContinue'
                'ErrorAction'   = 'Stop'
              }
              if ($PSBoundParameters.ContainsKey('Debug') -or $DebugPreference -eq 'Continue') {
                "Function: $($MyInvocation.MyCommand.Name) - Parameters (Set-TFPhoneNumber)", ($SetTFPhoneNumberParams | Format-Table -AutoSize | Out-String).Trim() | Write-Debug
              }
              Set-TFPhoneNumber @SetTFPhoneNumberParams
              $StatusMessage = "Number assigned to $ObjectType`: '$PhoneNumber'"
              Write-Information "SUCCESS: '$($CsUser.UserPrincipalName)' - $Operation`: OK - $StatusMessage"
            }
            catch {
              $ErrorLogMessage = "'$($CsUser.UserPrincipalName)' - $Operation`: Failed: $($_.Exception.Message)"
              $ErrorLog += $ErrorLogMessage
              Write-Information "FAILED: '$($CsUser.UserPrincipalName)' - $Operation`: Failed to execute"
              Write-Error -Message $ErrorLogMessage -ErrorAction $ErrorActionPreference
            }
          }
          else {
            # Phone Number is the same and Force is not used
            $StatusMessage = "'$PhoneNumber' present"
            Write-Verbose -Message "'$($CsUser.UserPrincipalName)' - $Operation`: $StatusMessage" -Verbose
          }
        }
        #endregion
      }
      else {
        $StatusMessage = 'Not provided'
        Write-Verbose -Message "NOSCOPE: '$($CsUser.UserPrincipalName)' - $Operation`: $StatusMessage" -Verbose
      }
      #endregion
    }
    #endregion

    $StatusID0 = 'Validation & Output'
    #region Log & Output
    # Write $ErrorLog
    if ( $WriteErrorLog.IsPresent -and $errorLog) {
      $CurrentOperationID0 = 'Writing ErrorLog'
      Write-BetterProgress -Id 0 -Activity $ActivityID0 -Status $StatusID0 -CurrentOperation $CurrentOperationID0 -Step ($private:CountID0++) -Of $private:StepsID0
      Write-TFErrorLog -ErrorLog $ErrorLog -Artifact $($CsUser.UserPrincipalName)
      return
    }
    else {
      Write-Verbose -Message "'$($CsUser.UserPrincipalName)' - No errors encountered! No log file written."
    }


    # Output
    $UserObjectPost = $null
    if ( $PassThru ) {
      # Re-Query Object
      $CurrentOperationID0 = 'Waiting for Office 365 to write the Object'
      Write-BetterProgress -Id 0 -Activity $ActivityID0 -Status $StatusID0 -CurrentOperation $CurrentOperationID0 -Step ($private:CountID0++) -Of $private:StepsID0
      Write-Verbose -Message 'Waiting 3-5s for Office 365 to write changes to User Object (Policies might not show up yet)' -Verbose
      Start-Sleep -Seconds 3
      $UserObjectPost = Get-TeamsUserVoiceConfig -UserPrincipalName $($CsUser.UserPrincipalName) -InformationAction SilentlyContinue -WarningAction SilentlyContinue
      if ( $PsCmdlet.ParameterSetName -eq 'DirectRouting' -and $null -eq $UserObjectPost.OnlineVoiceRoutingPolicy) {
        Start-Sleep -Seconds 2
        $UserObjectPost = Get-TeamsUserVoiceConfig -UserPrincipalName $($CsUser.UserPrincipalName) -InformationAction SilentlyContinue -WarningAction SilentlyContinue
      }
      if ( $PsCmdlet.ParameterSetName -eq 'DirectRouting' -and $null -eq $UserObjectPost.OnlineVoiceRoutingPolicy) {
        Write-Warning -Message 'Applied Policies take some time to show up on the object. Please verify again with Get-TeamsUserVoiceConfig'
      }
    }
    Write-Progress -Id 0 -Activity $ActivityID0 -Completed
    Write-Output $UserObjectPost

    #endregion

  } #process

  end {
    Write-Verbose -Message "[END ] $($MyInvocation.MyCommand)"
  } #end
} #Set-TeamsUserVoiceConfig