Public/Functions/VoiceConfig/New-TeamsVoiceRoutingChain.ps1

# Module: TeamsFunctions
# Function: Tenant Voice Configuration
# Author: David Eberhardt
# Updated: 19-FEB-2022
# Status: RC




function New-TeamsVoiceRoutingChain {
  <#
  .SYNOPSIS
    Creates Voice Routing configuration existing Pstn Gateways
  .DESCRIPTION
    Creates OVP, OPU & OVR for all provided Gateways. A 1:1:1:1 connection is created.
    Use Connect-VoiceRoutingChain to establish link if not successful
  .PARAMETER Name
    Required String. Proto-Name for the Routing Chain: Policy, PstnUsage and Voice Route.
    Prefixes, Interfixes and Suffixes are attached afterwards depending on Artifact, Country, Site & Call Restriction
  .PARAMETER Gateway
    Required String. OnlinePstnGateway used to connect the route for. Routes are created in order of provided Gateways.
    One or more FQDNs of OnlinePstnGateways already present and enabled in the Teams Tenant.
  .PARAMETER Region
    Optional selection of AMER, EMEA or APAC. Useful to indicate global geo-regional breakout
    Particle is added to the full name after the prefix and before the Name
  .PARAMETER Country
    String. ISO 3166-alpha2 or -alpha3 2/3-digit Country Code - Will be verified
    Required for Local Breakout and if non-default CallRestrictions are to be applied
  .PARAMETER Site
    Optional String (3-10 digits). Indicates a local breakout at a specific Site, adding the String to the Policy Name
    Required for Local Breakout and if non-default CallRestrictions are to be applied
  .PARAMETER CallRestriction
    Optional String. Indicates a Restriction to be applied to the Routes
    Level of Call Restriction to be applied: Unrestricted (default), International, National, EmergencyOnly, Custom
    Optional. Requires Country if not Unrestricted.
  .PARAMETER CustomRestrictionName
    Required String (3-10 digits) for Custom CallRestrictions only.
  .PARAMETER MatchingPattern
    Required for Custom CallRestrictions or to override specific restrictions
    Regex Pattern to match dialed numbers to. Required for Non-default CallRestrictions
  .EXAMPLE
    New-TeamsVoiceRoutingChain -Name TDR -Gateway PstnGatewayFqdn1.domain.com -Region AMER
    Creates one route per Gateway, one PstnUsage and one Policy: OVP-AMER-TDR
    No call restriction is applied. The Voice Route will receive a Matching pattern of ".*""
  .EXAMPLE
    New-TeamsVoiceRoutingChain -Name TDR -Gateway PstnGatewayFqdn1.domain.com -Country CA -CallRestriction Unrestricted
    Creates one route per Gateway, one PstnUsage and one Policy: OVP-TDR-CA (Unrestricted is the default Policy)
    Call Restrictions (except Unrestricted) can only be used with/for a country as the dial string is dependent on location.
    No call restriction is applied. The Voice Route will receive a Matching pattern of ".*""
  .EXAMPLE
    New-TeamsVoiceRoutingChain -Name TDR -Gateway PstnGatewayFqdn1.domain.com -Country CA -Site Toronto
    Creates one route per Gateway, one PstnUsage and one Policy: OVP-TDR-CA-Toronto
  .EXAMPLE
    New-TeamsVoiceRoutingChain -Name TDR -Gateway PstnGatewayFqdn1.domain.com,PstnGatewayFqdn2.domain.com -Country CA -CallRestriction National
    Creates one route per Gateway, one PstnUsage and one Policy: OVP-TDR-CA-National
    Applies CallRestriction to the Route with a Matching Pattern based on the dial code for the Country, i.E. "^\+1"
    NOTE: This does not take non-normalised calls to Emergency Services or other Services providers into Account.
    If this is a requirement, please amend the Matching pattern manually or use the next example to provide it
  .EXAMPLE
    New-TeamsVoiceRoutingChain -Name TDR -Gateway PstnGatewayFqdn1.domain.com,PstnGatewayFqdn2.domain.com -Country CA -Site Toronto -CallRestriction National -MatchingPattern "^\+1"
    Creates one route per Gateway, one PstnUsage and one Policy: OVP-TDR-CA-Toronto-National
    Applies CallRestriction to the Route with the Matching Pattern provided
  .INPUTS
    System.String
  .OUTPUTS
    System.Object
  .NOTES
    Naming Convention: The provided name is interpreted as a proto-name. The final name is constructed as follows:
    Each artifact is prefixed indicating their use: "OVP-" for Online Voice Routing Policies, "OPU-" for Online Pstn Usages
    "OVR-" for Online Voice Routes. OVRs are suffixed with an index if multiple Gateways are provided
    Full naming flow: <Artifact>["-"<Region>]"-"<Name>["-"<Country>]["-"<Site>]["-"<CallRestriction>]["-"<Index>]
    For Example: Name: "Standard", Region: "EMEA", Country: "France", Site: "Paris", CallRestriction: "National" with two Gateways provided
    Routing Policy: OVP-EMEA-Standard-FRA-Paris-National
    Pstn Usage: OPU-EMEA-Standard-FRA-Paris-National
    Voice Route: OVR-EMEA-Standard-FRA-Paris-National-1, OVR-EMEA-Standard-FRA-Paris-National-2
    The first Gateway will be attached to Voice Route 1, the second Gateway to Voice Route 2, etc.
  .COMPONENT
    Tenant Voice Routing
  .FUNCTIONALITY
    Direct Routing
  .LINK
    https://github.com/DEberhardt/TeamsFunctions/tree/master/docs/New-TeamsVoiceRoutingChain.md
  .LINK
    https://github.com/DEberhardt/TeamsFunctions/tree/master/docs/about_VoiceConfiguration.md
  .LINK
    https://github.com/DEberhardt/TeamsFunctions/tree/master/docs/
  #>


  [CmdletBinding(DefaultParameterSetName = 'Unrestricted', SupportsShouldProcess, ConfirmImpact = 'Medium')]
  [Alias('New-TeamsVRC')]
  [OutputType([System.Object])]
  param(
    [Parameter(Mandatory, HelpMessage = 'Proto Name for the Routing Chain')]
    [ValidatePattern('^([A-z-_+])*[^\s]\1*$')]
    [String]$Name,

    [Parameter(Mandatory, HelpMessage = 'FQDN of the Gateway(s) to be targeted')]
    [ValidatePattern('(?=^.{1,254}$)(^(?:(?!\d+\.)[a-zA-Z0-9_\-]{1,63}\.?)+(?:[a-zA-Z]{2,})$)')]
    [String[]]$Gateway,

    [Parameter(HelpMessage = 'Geo-Region to be added as a particle')]
    [ValidateSet('AMER', 'EMEA', 'APAC')]
    [String]$Region,

    [Parameter(ParameterSetName = 'Unrestricted', HelpMessage = 'ISO 3166-alpha2 Country Code')]
    [Parameter(Mandatory, ParameterSetName = 'Restricted', HelpMessage = 'ISO 3166-alpha2 Country Code')]
    [Parameter(Mandatory, ParameterSetName = 'RestrictedCustom', HelpMessage = 'ISO 3166-alpha2 Country Code')]
    [ValidatePattern('^([A-Z]){2,3}$')]
    [string]$Country,

    [Parameter(ParameterSetName = 'Unrestricted', HelpMessage = 'Creates Site-Specific policies')]
    [Parameter(ParameterSetName = 'Restricted', HelpMessage = 'Creates Site-Specific policies')]
    [Parameter(Mandatory, ParameterSetName = 'RestrictedCustom', HelpMessage = 'Creates Site-Specific policies')]
    [ValidateLength(3, 10)]
    [String]$Site,

    [Parameter(Mandatory, ParameterSetName = 'Restricted', HelpMessage = 'Call Restriction Level')]
    [Parameter(Mandatory, ParameterSetName = 'RestrictedCustom', HelpMessage = 'Call Restriction Level')]
    [ValidateSet('Unrestricted', 'International', 'National', 'EmergencyOnly', 'Custom')]
    [String]$CallRestriction,

    [Parameter(Mandatory, ParameterSetName = 'RestrictedCustom', HelpMessage = 'Custom Name for the Call Restriction Level')]
    [ValidateLength(3, 10)]
    [ValidatePattern('^([A-z])*[^\s]\1*$')]
    [String]$CustomRestrictionName,

    [Parameter(ParameterSetName = 'Restricted', HelpMessage = 'Matching pattern for the Voice Route')]
    [Parameter(Mandatory, ParameterSetName = 'RestrictedCustom', HelpMessage = 'Matching pattern for the Voice Route')]
    [ValidateScript( { ('.*' -or ($_.Substring(0, 1) -eq '^')) })]
    [String]$MatchingPattern
  )
  begin {
    Show-FunctionStatus -Level RC
    Write-Verbose -Message "[BEGIN ] $($MyInvocation.MyCommand.Name)"
    Write-Verbose -Message "Need help? Online: $global:TeamsFunctionsHelpURLBase$($MyInvocation.MyCommand.Name)`.md"
    # 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

    #region Data Validation
    $StatusID0 = 'Data validation'
    $CurrentOperationID0 = 'Processing Call Restrictions'
    Write-BetterProgress -Id 0 -Activity $ActivityID0 -Status $StatusID0 -CurrentOperation $CurrentOperationID0 -Step ($private:CountID0++) -Of $private:StepsID0

    if (-not $PSBoundParameters.ContainsKey('CallRestriction')) {
      $CallRestriction = 'Unrestricted'
      Write-Information "INFO: Parameter CallRestriction not provided. Using: '$CallRestriction'"
    }
    else {
      Write-Information "INFO: Parameter CallRestriction provided. Using: '$CallRestriction'"
    }

    $CurrentOperationID0 = 'Processing Matching Pattern'
    Write-BetterProgress -Id 0 -Activity $ActivityID0 -Status $StatusID0 -CurrentOperation $CurrentOperationID0 -Step ($private:CountID0++) -Of $private:StepsID0

    if ( $CallRestriction -eq 'Unrestricted') {
      if ( $MatchingPattern ) {
        Write-Verbose -Message "MatchingPattern '$MatchingPattern' will be ignored. For Call Restriction '$CallRestriction' the Matching Pattern is Hardcoded." -Verbose
      }
      $MatchingPattern = '.*'
    }
    elseif ( $PSBoundParameters.ContainsKey('MatchingPattern') -and -not $PSBoundParameters.ContainsKey('Country') ) {
      #NOTE THis may not even be needed as the Parameterset is taking care of the input. If parameterset does not work, this will be revisited
      Write-Error -Message 'CallRestrictions with Matching Patterns can only be deployed for a specific country. Please drop MatchingPattern or provide a Country'
      return
    }
    Write-Information "INFO: MatchingPattern used: '$MatchingPattern'"

    if ( -not $MatchingPattern ) {
      Write-Information "Validation: Parameter MatchingPattern NOT provided for Call Restriction '$CallRestriction' in Country '$Country'."
      if ( $CallRestriction -ne 'Unrestricted') {
        #TODO Buildout for Querying other function that contains Dial Code and constructing NAT and INT respectively
        #122 UCDialPlans API access can be plugged in here - via Helper functions
        #INT: ".*"" if nothing else is provided
        #NAT: "^\+XX" for all calls if nothing else is provided

        Write-Error -Message "Parameter MatchingPattern not provided, no data for Call Restriction '$CallRestriction'"
        return
      }
    }
    else {
      if ( $Country ) {
        Write-Information "INFO: Parameter MatchingPattern provided for Call Restriction '$CallRestriction' in Country '$Country': Using: '$MatchingPattern'"
      }
      else {
        #TODO additional block here? Write-Error and Return as above?
        Write-Information "INFO: Parameter MatchingPattern provided for Call Restriction '$CallRestriction': Using: '$MatchingPattern'"
      }
    }

    $CurrentOperationID0 = 'Processing Country'
    Write-BetterProgress -Id 0 -Activity $ActivityID0 -Status $StatusID0 -CurrentOperation $CurrentOperationID0 -Step ($private:CountID0++) -Of $private:StepsID0

    if ($PSBoundParameters.ContainsKey('Country')) {
      try {
        $CountryObject = switch ( $Country.length ) {
          2 { Get-ISO3166Country | Where-Object TwoLetterCode -EQ $Country }
          3 { Get-ISO3166Country | Where-Object ThreeLetterCode -EQ $Country }
          default { $null }
        }
        if ($null -eq $CountryObject) { throw }
        if ($PSBoundParameters.ContainsKey('Debug')) {
          " Function: $($MyInvocation.MyCommand.Name) - CountryObject:", ($CountryObject | Format-Table -AutoSize | Out-String).Trim() | Write-Debug
        }
      }
      catch {
        Write-Error "Country '$Country' not found as an ISO3166 Country Code. Please provide country code in alpha-2 or alpha-3 notation (find with Get-ISO3166Country)" -ErrorAction Stop
      }
      try {
        # Determining Region
        #CHECK whether this should not be done later if no input for REGION has been determined
        $SiteRegion = Get-RegionFromCountryCode -Country $CountryObject.TwoLetterCode
        $CountryCode = switch ( $Country.length ) {
          2 { $CountryObject.TwoLetterCode }
          3 { $CountryObject.ThreeLetterCode }
          default { $null }
        }

        if ($PSBoundParameters.ContainsKey('Debug')) {
          " Function: $($MyInvocation.MyCommand.Name) - Determined SiteRegion:", ($SiteRegion | Format-Table -AutoSize | Out-String).Trim() | Write-Debug
        }
      }
      catch {
        Write-Error "Site Region not determined for Country '$Country'" -ErrorAction Stop
      }
    }

    $CurrentOperationID0 = 'Processing Region'
    Write-BetterProgress -Id 0 -Activity $ActivityID0 -Status $StatusID0 -CurrentOperation $CurrentOperationID0 -Step ($private:CountID0++) -Of $private:StepsID0

    if (-not $Region) {
      if ($CountryCode) {
        $Region = $SiteRegion
        Write-Information "INFO: Site Region determined. Using: '$Region'"
      }
      else {
        Write-Error 'Region not determined. Please provide Region'
        return
      }
    }
    else {
      if ($Region -ne $SiteRegion) {
        Write-Warning -Message "Determined Site Region '$SiteRegion' and provided Region '$Region' do not match, this may be deliberate or an error."
      }
      Write-Information "INFO: Parameter Region provided. Using: '$Region'"
    }
    #endregion

    #region Clean up of provided Name - Normalization step to reconstruct Policy Name proper
    $CurrentOperationID0 = 'Name normalization'
    Write-BetterProgress -Id 0 -Activity $ActivityID0 -Status $StatusID0 -CurrentOperation $CurrentOperationID0 -Step ($private:CountID0++) -Of $private:StepsID0
    #Checking for accidental duplication of particles in Name: Country
    if ( $Name -match "(-|_)(?<Country>($Country|$CountryCode))" ) {
      $Name = $Name -replace "^(.*?)((-|_)($($Matches.Country)))(.*)?$", '$1$5'
      Write-Verbose -Message "Country '$($Matches.Country)' discovered in provided Name. String deconstructed to '$Name'" -Verbose
    }
    #Checking for accidental duplication of particles in Name: Region
    if ( $Name -match "(-|_)(?<Region>(EMEA|AMER|APAC|$Region))" ) {
      $Name = $Name -replace "^(.*?)((-|_)($($Matches.Region)))(.*)?$", '$1$5'
      Write-Verbose -Message "Region '$($Matches.Region)' discovered in provided Name. String deconstructed to '$Name'" -Verbose
    }
    #Checking for accidental duplication of particles in Name: Call Restriction
    $RestrictionMatchString = "((-|_)(?<Restriction>(Unrestricted|International|National|EmergencyOnly|Restricted|$(if( $CustomRestrictionName ) { $CustomRestrictionName } else { 'Custom' }))))"
    if ( $Name -match $RestrictionMatchString ) {
      $Name = $Name -replace "^(.*?)(-|_)?($($matches.Restriction))$", '$1'
      Write-Verbose -Message "CallRestriction '$($matches.Restriction)' discovered in provided Name. String deconstructed to '$Name'" -Verbose
    }
    #Checking for accidental duplication of particles in Name: OVP Prefix
    if ( $Name -match '^(?<OVPprefix>OVP)(-|_)' ) {
      $Name = $Name -replace "^($($Matches.OVPprefix))(-|_)?(.*)?$", '$3'
      Write-Verbose -Message "Prefix '$($Matches.OVPprefix)' discovered in provided Name. String deconstructed to '$Name'" -Verbose
    }

    #Creating Policy Name
    $CurrentOperationID0 = 'Creating Proto String'
    Write-BetterProgress -Id 0 -Activity $ActivityID0 -Status $StatusID0 -CurrentOperation $CurrentOperationID0 -Step ($private:CountID0++) -Of $private:StepsID0

    [string]$ProtoString = "$(if ( $Region ) { $Region })" + "$(if ( $Region ) { '-' })" + "$Name" + `
      "$(if ( $CountryCode -or $CallRestriction -ne 'Unrestricted') { '-' + $CountryCode })" + `
      "$(if ( $Site ) { '-' + $Site })" + `
      "$(if ( $CallRestriction -ne 'Unrestricted') { '-' + $(if ($CallRestriction -eq 'Custom') { $CustomRestrictionName } else { $CallRestriction }) })"
    if ($PSBoundParameters.ContainsKey('Debug')) {
      " Function: $($MyInvocation.MyCommand.Name) - ProtoString: '$ProtoString'" | Write-Debug
    }
    Write-Information "INFO: Common String used across OVP, OPU & OVR: '$ProtoString' - This will be pre-fixed depending on the artifact"

    # Constructing Names
    $PolicyName = 'OVP-' + $ProtoString
    $UsageName = 'OPU-' + $ProtoString
    $RouteName = 'OVR-' + $ProtoString
    Write-Verbose -Message "OVP Name: '$PolicyName'"
    Write-Verbose -Message "OPU Name: '$UsageName'"
    Write-Verbose -Message "OVR Name: '$RouteName' (suffix will be attached if multiple Gateways are specified)"
    #endregion
  }

  Process {
    # Initialising Objects
    [System.Collections.ArrayList]$Usages = @()
    [System.Collections.ArrayList]$Routes = @()
    [System.Collections.ArrayList]$Policies = @()

    $StatusID0 = 'Processing'
    $CurrentOperationID0 = 'Online Voice Routes'
    Write-BetterProgress -Id 0 -Activity $ActivityID0 -Status $StatusID0 -CurrentOperation $CurrentOperationID0 -Step ($private:CountID0++) -Of $private:StepsID0

    #region Construction
    # Defining one Policy for Local breakout
    # Constructing Routes per Gateway
    $GatewayCounter = 0
    [int]$GatewayCount = $Gateway.Count
    foreach ($GW in $Gateway) {
      # Fetching Gateways
      try {
        $OnlinePstnGateway = Get-CsOnlinePSTNGateway -Identity $GW -ErrorAction Stop
      }
      catch {
        Write-Error -Message "OnlinePstnGateway '$GW' not found. No route is being created. Please check Gateway name" -Category InvalidData
        continue
      }

      # Counter
      $GatewayCounter++
      $Index = if ( $GatewayCount -gt 1 -and $GatewayCounter -le $GatewayCount) { $GatewayCounter } else { $null }
      if ( $Index ) {
        Write-Verbose -Message "Voice Route for Gateway '$GW' - Creating index suffix: '-$Index'"
      }

      # Constructing Route
      $Description = $(if ($Region) { $Region + ' - ' }) + $(if ($CountryCode) { $CountryObject.Name }) + $(if ($Site) { ', ' + $Site }) + $(if ( $CallRestriction -ne 'Unrestricted' ) { ', Restricted to ' + $CallRestriction })
      $CsOnlineVoiceRoute = $null
      $CsOnlineVoiceRoute = @{
        Name                  = $RouteName + $( if ($Index) { '-' + $Index })
        Description           = 'Route for ' + $Description + $( if ($Index) { ' - ' + $Index })
        NumberPattern         = $MatchingPattern
        OnlinePstnUsages      = @{add = $UsageName }
        OnlinePstnGatewayList = $OnlinePstnGateway.Fqdn
      }
      if ($PSBoundParameters.ContainsKey('Debug')) {
        " Function: $($MyInvocation.MyCommand.Name) - CsOnlineVoiceRoute:", ($CsOnlineVoiceRoute | Format-Table -AutoSize | Out-String).Trim() | Write-Debug
      }

      # Constructing Usage
      if ( $CsOnlineVoiceRoute.OnlinePstnGatewayList.Count -gt 0 ) {
        [Void]$Routes.Add($CsOnlineVoiceRoute)
        Write-Verbose -Message "Online Voice Route '$($CsOnlineVoiceRoute.Name)' added"
        if ( $Usages -notcontains $UsageName) {
          if ($PSBoundParameters.ContainsKey('Debug')) {
            " Function: $($MyInvocation.MyCommand.Name) - CsOnlinePstnUsage(Global):", ($UsageName | Format-Table -AutoSize | Out-String).Trim() | Write-Debug
          }
          # Avoiding duplication errors
          [Void]$Usages.Add($UsageName)
          Write-Verbose -Message "Online Pstn Usage '$UsageName' added"
        }
      }
      else {
        Write-Warning -Message "Online Voice Route '$($CsOnlineVoiceRoute.Name)' not added - No corresponding Gateway '$GW' found"
      }
    }

    # Constructing Policy
    $CurrentOperationID0 = 'Online Voice Routing Policies'
    Write-BetterProgress -Id 0 -Activity $ActivityID0 -Status $StatusID0 -CurrentOperation $CurrentOperationID0 -Step ($private:CountID0++) -Of $private:StepsID0
    $CsOnlineVoiceRoutingPolicy = $null
    $CsOnlineVoiceRoutingPolicy = @{
      Identity         = $PolicyName
      OnlinePstnUsages = $CsOnlineVoiceRoute.OnlinePstnUsages # Test $UsageName instead?
      Description      = $Description
    }
    if ($PSBoundParameters.ContainsKey('Debug')) {
      " Function: $($MyInvocation.MyCommand.Name) - CsOnlineVoiceRoutingPolicy:", ($CsOnlineVoiceRoutingPolicy | Format-Table -AutoSize | Out-String).Trim() | Write-Debug
    }
    if ( $Policies.Identity -notcontains $CsOnlineVoiceRoutingPolicy.Identity) {
      [Void]$Policies.Add($CsOnlineVoiceRoutingPolicy)
      Write-Verbose -Message "Online Voice Routing Policy '$($CsOnlineVoiceRoutingPolicy.Identity)' added"
    }
    #endregion

    #region Action
    $StatusID0 = 'Creating'
    # 1 Add Usage
    $CurrentOperationID0 = 'Online Pstn Usages'
    Write-BetterProgress -Id 0 -Activity $ActivityID0 -Status $StatusID0 -CurrentOperation $CurrentOperationID0 -Step ($private:CountID0++) -Of $private:StepsID0
    if ($PSCmdlet.ShouldProcess("$Usages", 'Set-CsOnlinePstnUsage')) {
      #foreach ($OPU in $Policies.OnlinePstnUsages) {
      foreach ($OPU in $Usages) {
        if ($PSBoundParameters.ContainsKey('Debug')) {
          " Function: $($MyInvocation.MyCommand.Name) - OPU:", ($OPU | Format-Table -AutoSize | Out-String).Trim() | Write-Debug
        }
        try {
          #TODO This caused a BUG in one of the recent tests, but has worked before! PSlistModifier issue to be addressed!
          $null = Set-CsOnlinePstnUsage Global -Usage @{ add = "$OPU" } -ErrorAction Stop
          Write-Information "INFO: Online Pstn Usage '$OPU' created"
        }
        catch {
          if ($_.Exception.Message.Contains('There is a duplicate key sequence')) {
            Write-Warning -Message "Online Pstn Usage '$UsageName' already exists - No Action taken"
          }
          else {
            Write-Error "$($_.Exception.Message)" -ErrorAction Stop
            continue
          }
        }
      }
    }

    # 2 Create Route add to Usage
    $CurrentOperationID0 = 'Online Voice Routes'
    Write-BetterProgress -Id 0 -Activity $ActivityID0 -Status $StatusID0 -CurrentOperation $CurrentOperationID0 -Step ($private:CountID0++) -Of $private:StepsID0
    if ($PSCmdlet.ShouldProcess("$($Routes.Name)", 'New-CsOnlineVoiceRoute')) {
      foreach ($OVR in $Routes) {
        try {
          $CreatedRoute = $null
          $CreatedRoute = New-CsOnlineVoiceRoute @OVR -ErrorAction Stop
          Write-Information "INFO: Online Voice Route '$($CreatedRoute.Identity)' created"
        }
        catch {
          if ($_.Exception.Message.Contains('already exists')) {
            Write-Warning -Message "Online Voice Route '$($CsOnlineVoiceRoute.Name)' already exists - No Action taken"
          }
          else {
            Write-Error "$($_.Exception.Message)"
          }
        }
      }
    }

    # 3 Create Policy, add to Usage
    # Creating the Policy while adding the Usage is dependent on O365 replication (this works immediately for Routes, but not for policies!?)
    # Attempting to add the Policy incl. the Usage, but if not successful, will just add the Policy and leave it unconnected.
    # Run this command again or Connect-VoiceRoutingChain to rectify
    $CurrentOperationID0 = 'Online Voice Routing Policies'
    Write-BetterProgress -Id 0 -Activity $ActivityID0 -Status $StatusID0 -CurrentOperation $CurrentOperationID0 -Step ($private:CountID0++) -Of $private:StepsID0
    if ($PSCmdlet.ShouldProcess("$($Policies.Identity)", 'New-CsOnlineVoiceRoutingPolicy')) {
      foreach ($OVP in $Policies) {
        try {
          $CreatedPolicy = $null
          $CreatedPolicy = New-CsOnlineVoiceRoutingPolicy @OVP -ErrorAction Stop
          Write-Information "INFO: Online Voice Routing Policy '$($CreatedPolicy.Identity)' created"
        }
        catch {
          if ($_.Exception.Message.Contains('already exists')) {
            $ExistingPolicy = Get-CsOnlineVoiceRoutingPolicy -Identity $OVP.Identity
            if ( $ExistingPolicy ) {
              Write-Warning -Message "Online Voice Routing Policy '$($OVP.Identity)' already exists - No Action taken"
            }
          }
          elseif ($_.Exception.Message.Contains('Cannot find specified Online PSTN usage')) {
            Write-Verbose -Message "Online Voice Routing Policy '$($OVP.Identity)' - PstnUsage cannot be found (yet) - retrying in 30s" -Verbose
            Start-Sleep -Seconds 30
            try {
              $CreatedPolicy = $null
              $CreatedPolicy = New-CsOnlineVoiceRoutingPolicy @OVP -ErrorAction Stop
              Write-Information "INFO: Online Voice Routing Policy '$($CreatedPolicy.Identity)' created"
            }
            catch {
              $OVP.Remove('OnlinePstnUsages')
              Write-Verbose -Message "Online Voice Routing Policy '$($OVP.Identity)' - PstnUsage cannot be found (yet) - Please re-run this command in 5-10 mins" -Verbose
              try {
                $CreatedPolicy = $null
                $CreatedPolicy = New-CsOnlineVoiceRoutingPolicy @OVP -ErrorAction Stop
                Write-Information "INFO: Online Voice Routing Policy '$($CreatedPolicy.Identity)' created without attaching PstnUsage"
                Write-Verbose -Message "Policy created without attaching the PstnUsage. Please allow for replication, then run 'Connect-VoiceRoutingChain'"
              }
              catch {
                Write-Error "$($_.Exception.Message)"
              }
            }
          }
          else {
            Write-Error "$($_.Exception.Message)"
          }
        }

        #Output
        #Get-CsOnlineVoiceRoutingPolicy $($OVP.Identity)
        $CurrentOperationID0 = 'Querying output object'
        Write-BetterProgress -Id 0 -Activity $ActivityID0 -Status $StatusID0 -CurrentOperation $CurrentOperationID0 -Step ($private:CountID0++) -Of $private:StepsID0
        Get-TeamsVoiceRoutingChain $($OVP.Identity)
        Write-Progress -Id 0 -Activity $ActivityID0 -Completed
      }
    }
    #endregion

  }

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