Public/Support/VoiceConfig/Get-TeamsPhoneNumber.ps1

# Module: TeamsFunctions
# Function: Teams User Voice Configuration
# Author: David Eberhardt
# Updated: 02-JUN-2022
# Status: Live

#CHECK: How to use -ExpandLocation? - Currently broken?
# TEST OPeratorConnect numbers and output for it (NUmberType)

function Get-TeamsPhoneNumber {
  <#
  .SYNOPSIS
    Queries Phone Numbers in Teams and returns a concise result
  .DESCRIPTION
    Wrapper for Get-CsPhoneNumberAssignment to ascertain the NumberType or filter by it.
    If a number is not found in the tenant it is assumed to be of the Type DirectRouting and an object is returned.
    For exact matches, the potential assignment is queried in addition to the boolean assignment status.
  .PARAMETER PhoneNumber
    Returns an Object abbreviated from Get-CsPhoneNumberAssignment of the type CallingPlan or OperatorConnect.
    Returns a mocked Object for DirectRouting Numbers as these numbers are not present in the tenant.
    Expected format is E.164 or LineUri. Requires full number. For more granular search use Get-CsPhoneNumberAssignment.
  .PARAMETER NumberType
    Valid values are CallingPlan, OperatorConnect or DirectRouting
    Using DirectRouting will return an empty object as these numbers are not present in the tenant.
  .PARAMETER Location
    Returns an Object abbreviated from Get-CsPhoneNumberAssignment of the type CallingPlan or OperatorConnect.
    Requires the LocationId or Location Name (DisplayName).
  .PARAMETER City
    Returns an Object abbreviated from Get-CsPhoneNumberAssignment of the type CallingPlan or OperatorConnect.
    Requires the City as displayed in Get-CsPhoneNumberAssignment.
    For DirectRouting this value will always be "All Locations" and cannot be queried by using this switch.
  .PARAMETER CapabilitiesContain
    Returns an Object abbreviated from Get-CsPhoneNumberAssignment.
    Available options are: ConferenceAssignment, VoiceApplicationAssignment, UserAssignment
  .PARAMETER IsNotAssigned
    Only returns free numbers
  .EXAMPLE
    Get-TeamsPhoneNumber -PhoneNumber +15551234567
 
    Returns one Phone Number Object for this string
    The Object returned is an abbreviation and extension of the Output displayed with Get-CsPhoneNumberAssignment.
  .EXAMPLE
    Get-TeamsPhoneNumber -PhoneNumber +1555*
 
    Returns all Phone Number Objects found starting with this string
  .EXAMPLE
    Get-TeamsPhoneNumber -NumberType OperatorConnect
 
    Returns all Phone Numbers added to the tenant via OperatorConnect. Also available: CallingPlan
  .EXAMPLE
    Get-TeamsPhoneNumber -Location 00000000-0000-0000-0000-000000000000
 
    Returns all Phone Numbers present in the tenant that matches this LocationID (not the CivicAddressID)
    Available only for Numbers of PhoneNumberType CallingPlan
  .EXAMPLE
    Get-TeamsPhoneNumber -Location "R&D Building"
 
    Returns all Phone Numbers present in the tenant that matches the location with the Description "R&D Building"
    Search by location is only possible for Numbers of PhoneNumberType CallingPlan.
    For OperatorConnect the Location is set by the Operator and cannot be changed
  .EXAMPLE
    Get-TeamsPhoneNumber -City Auckland
 
    Returns all Phone Numbers present in the tenant that matches the City provided
    The City is required to be provided as displayed with Get-CsPhoneNumberAssignment
  .EXAMPLE
    Get-TeamsPhoneNumber -CapabilitiesContain UserAssignment
 
    Returns all Phone Numbers present in the tenant that can be assigned to a user
  .EXAMPLE
    Get-TeamsPhoneNumber -CapabilitiesContain UserAssignment -PhoneNumber +1555* -Location "R&D Building"
 
    Returns all Phone Numbers present in the tenant that match all criteria, i.E. with the Capability to assign to User,
    the phone number starting with 1555 (* performs search), and the location translated from the description "R&D Building"
  .INPUTS
    System.String
  .OUTPUTS
    System.Object
  .NOTES
    Simple helper function to query a Phone Number and receive its NumberType.
    Other functionality is purely wrapped around Get-CsPhoneNumberAssignment to provide a rounded picture
    For full information, please use Get-CsPhoneNumberAssignment
    The corresponding Set-Cmdlet Set-TeamsPhoneNumber also supports the NumberType 'DirectRouting'.
    For searches by PhoneNumber, the CmdLet will interpret any number not found in the tenant to be of the type
    DirectRouting and return it as such.
  .COMPONENT
    VoiceConfiguration
  .FUNCTIONALITY
    Queries information about Phone Numbers in the Teams Tenant to inform Voice Configuration
  .LINK
    https://github.com/DEberhardt/TeamsFunctions/tree/main/docs/Get-TeamsPhoneNumber.md
  .LINK
    https://github.com/DEberhardt/TeamsFunctions/tree/main/docs/about_VoiceConfiguration.md
  .LINK
    https://github.com/DEberhardt/TeamsFunctions/tree/main/docs/about_UserManagement.md
  .LINK
    https://github.com/DEberhardt/TeamsFunctions/tree/main/docs/about_Supporting_Functions.md
  .LINK
    https://github.com/DEberhardt/TeamsFunctions/tree/main/docs/
  #>


  [CmdletBinding(ConfirmImpact = 'Low')]
  [OutputType([System.Object])]
  param(
    [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName)]
    [Alias('Number', 'TelephoneNumber')]
    [AllowNull()]
    [AllowEmptyString()]
    [string]$PhoneNumber,

    [Parameter(ValueFromPipelineByPropertyName)]
    [Alias('PhoneNumberType', 'Type')]
    [ValidateSet('CallingPlan', 'OperatorConnect', 'DirectRouting')]
    [string]$NumberType,

    [Parameter(ValueFromPipelineByPropertyName)]
    [Alias('LocationId', 'LocationName')]
    [string]$Location,

    [Parameter(ValueFromPipelineByPropertyName)]
    [string]$City,

    [Parameter(ValueFromPipelineByPropertyName)]
    [ValidateSet('UserAssignment', 'VoiceApplicationAssignment', 'ConferenceAssignment')]
    [string]$CapabilitiesContain,

    [Parameter(ValueFromPipelineByPropertyName)]
    [switch]$IsNotAssigned
  ) #param

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

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

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

    if ( [String]::IsNullOrEmpty($PhoneNumber) ) {
      $PhoneNumber = $null
    }

    # Preparing Splatting Object
    $parameters = $null
    $Parameters = @{
      'WarningAction' = 'SilentlyContinue'
      'ErrorAction'   = 'Stop'
    }

    if ($PhoneNumber) {
      #Normalising Phonenumber before call
      $PhoneNumberSearchMode = if ( $PhoneNumber -match '\*$' ) { $true } else { $false }
      $PhoneNumber = $PhoneNumber | Format-StringForUse -As Number
      if ( $PhoneNumberSearchMode ) {
        Write-Verbose -Message "PhoneNumber interpreted as part of the string (searching all Phone Numbers starting with '$($PhoneNumber.Replace('*',''))'"
        $Parameters.TelephoneNumberStartsWith = $PhoneNumber
      }
      else {
        Write-Verbose -Message "PhoneNumber interpreted as full number (searching for exact match of '$($PhoneNumber.Replace('*',''))'"
        $Parameters.TelephoneNumber = $PhoneNumber
      }
    }
    if ($PSBoundParameters.ContainsKey('Location')) {
      Write-Verbose -Message "Location '$Location' queried against Get-CsOnlineLisCivicAddress"
      try {
        $CsOnlineLisLocation = if ( $Address -match $script:TFMatchGuid ) {
          Get-CsOnlineLisLocation -LocationId $Address -ErrorAction Stop
        }
        else {
          Get-CsOnlineLisLocation -Location "$Address" -ErrorAction Stop
        }
        $Parameters.Location = $CsOnlineLisLocation.LocationId
      }
      catch {
        throw "Location '$Location' not found (CsOnlineLisLocation or CsOnlineLisCivicAddress)! Please provide CsOnlineLisLocation,CsOnlineLisCivicAddress or Description"
      }
    }
    if ($PSBoundParameters.ContainsKey('City')) { $Parameters.City = $City }
    if ($PSBoundParameters.ContainsKey('CapabilitiesContain')) { $Parameters.CapabilitiesContain = $CapabilitiesContain }
    #IsNotAssigned is not processed here, but after the query

  } #begin

  process {
    Write-Verbose -Message "[PROCESS] $($MyInvocation.MyCommand)"
    try {
      if ($PSBoundParameters.ContainsKey('Debug') -or $DebugPreference -eq 'Continue') {
        "Function: $($MyInvocation.MyCommand.Name) - Parameters (Get-CsPhoneNumberAssignment)", ($Parameters | Format-Table -AutoSize | Out-String).Trim() | Write-Debug
      }
      $Numbers = Get-CsPhoneNumberAssignment @Parameters

      if ($PSBoundParameters.ContainsKey('Debug') -or $DebugPreference -eq 'Continue') {
        "Function: $($MyInvocation.MyCommand.Name) - Numbers", ($Numbers | Format-Table -AutoSize | Out-String).Trim() | Write-Debug
      }
    }
    catch {
      Write-Error -Message "Error querying Get-CsPhoneNumberAssignment: $($_.Exception.Message)"
    }

    # Exit strategy
    if ( -not $Numbers ) {
      if ( -not $PhoneNumber ) {
        Write-Information "INFO: No numbers found with these Parameters: $($PSBoundParameters.keys)"
        continue
      }
    }

    # Output
    if ( $Numbers ) {
      # OperatorConnect or CallingPlans Number detected

      # Filter by NumberType
      if ( $PSBoundParameters.ContainsKey('NumberType') ) {
        #TEST This may be able to be rebound to NumberType if it can be ascertained that it _does_ show OperatorConnect and not CallingPlans
        # $Numbers = $Numbers | Where-Object NumberType -eq $NumberType }
        switch ($NumberType) {
          #'OperatorConnect' { $Numbers = $Numbers | Where-Object NumberType -eq $NumberType }
          #'CallingPlan' { $Numbers = $Numbers | Where-Object NumberType -eq $NumberType }
          #'DirectRouting' { $Numbers = $Numbers | Where-Object NumberType -eq $NumberType }
          'OperatorConnect' { $Numbers = $Numbers | Where-Object PstnPartnerName -NE 'Microsoft' }
          'CallingPlan' { $Numbers = $Numbers | Where-Object PstnPartnerName -EQ 'Microsoft' }
          'DirectRouting' { $Numbers = $Numbers | Where-Object PstnPartnerName -EQ '' }
        }
      }

      # Filtering by assignment
      if ($PSBoundParameters.ContainsKey('IsNotAssigned')) {
        $Numbers = $Numbers | Where-Object { $_.PstnAssignmentStatus -EQ 'Unassigned' }
      }

      foreach ($Number in $Numbers) {
        # Translating parameters
        $NumberLocationName = $null
        $NumberLocationName = if ( $Number.LocationId -NE '00000000-0000-0000-0000-000000000000' ) { (Get-CsOnlineLisLocation -LocationId $Number.LocationId).Description } else { $null }
        #TEST This may be able to be rebound to NumberType if it can be ascertained that it _does_ show OperatorConnect and not CallingPlans
        #$NumberNumberType = $Number.NumberType
        $NumberNumberType = switch ($Number.PstnPartnerName) {
          'Microsoft' { 'CallingPlan' }
          '' { 'DirectRouting' }
          Default { 'OperatorConnect' }
        }

        # Finding assignments
        $NumberAssignment = $null
        #VALIDATE Design with nested Objects may be detrimental for export - flattening to UPN, custom name
        switch ($Number.PstnAssignmentStatus) {
          'UserAssigned' {
            $NumberAssignment = Get-CsOnlineUser $Number.AssignedPstnTargetId -WarningAction SilentlyContinue
            #$NumberAssignedTo = $NumberAssignment | Select-Object UserPrincipalName, SipAddress, IsSipEnabled
            $NumberAssignedTo = $NumberAssignment.UserPrincipalName
            $NumberAssignedToSIP = $NumberAssignment.SipAddress
            $NumberAssignedToObjectType = 'User'
            Write-Verbose -Message 'AssignedTo is populated from the CsOnlineUser Object, displaying the UserPrincipalName'
          }
          'VoiceApplicationAssigned' {
            $NumberAssignment = Get-CsOnlineUser $Number.AssignedPstnTargetId -WarningAction SilentlyContinue
            #$NumberAssignedTo = $NumberAssignment | Select-Object UserPrincipalName, SipAddress, IsSipEnabled
            $NumberAssignedTo = $NumberAssignment.UserPrincipalName
            $NumberAssignedToSIP = $NumberAssignment.SipAddress
            $NumberAssignedToObjectType = 'ResourceAccount'
            Write-Verbose -Message 'AssignedTo is populated from the CsOnlineUser Object, displaying the UserPrincipalName'
          }
          'ConferenceAssigned' {
            $NumberAssignment = Get-CsOnlineDialInConferencingBridge $Number.AssignedPstnTargetId
            #$NumberAssignedTo = $NumberAssignment | Select-Object Region,Name,DefaultServiceNumber
            $NumberAssignedTo = "$($NumberAssignment.Region) $($NumberAssignment.Name) $($NumberAssignment.DefaultServiceNumber)"
            $NumberAssignedToSIP = $null
            $NumberAssignedToObjectType = 'ConferenceBridge'
            Write-Verbose -Message 'AssignedTo populated from the CsOnlineDialInConferencingBridge Object, displaying Region, Name & DefaultNumber (string)'
          }
          'Unassigned' { $NumberAssignment = $NumberAssignedToObjectType = $null }
          'Default' { $NumberAssignment = $NumberAssignedToObjectType = $null }
        }

        # Creating Output Object
        $PhoneNumberObject = $null
        $PhoneNumberObject = [TFPhoneNumberObject]@{
          PhoneNumber          = $Number.TelephoneNumber
          NumberType           = $NumberNumberType
          LocationId           = $Number.LocationId
          LocationName         = $NumberLocationName
          City                 = $Number.City
          Capability           = $Number.Capability
          Assigned             = [bool]$($NumberAssignment)
          AssignedTo           = $NumberAssignedTo
          AssignedToSIP        = $NumberAssignedToSIP
          AssignedToObjectType = $NumberAssignedToObjectType
          AssignedPstnTargetId = $Number.AssignedPstnTargetId
        }
        Write-Output $PhoneNumberObject
      }
    }
    else {
      # Assuming Number is not in the Tenant and therefore a DirectRouting Number
      $NumberAssignment = $null
      Write-Information 'INFO: Number not found in the tenant, this is expected for Direct Routing numbers.'

      # Creating Output Object
      $PhoneNumberObject = $null
      $PhoneNumberObject = [TFPhoneNumberObject]@{
        PhoneNumber          = $PhoneNumber
        NumberType           = 'DirectRouting'
        LocationId           = $null
        LocationName         = $null
        City                 = $null
        Capability           = @('ConferenceAssignment', 'VoiceApplicationAssignment', 'UserAssignment' )
        Assigned             = $false
        AssignedTo           = $null
        AssignedToSIP        = $null
        AssignedToObjectType = $null
        AssignedPstnTargetId = $null
      }

      Write-Output $PhoneNumberObject
    }
  } #process

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