Public/VoiceConfig/New-TeamsVoiceRoutingChain.ps1

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

# Add ArgumentCompleter for Gateway and validate Others
# Rework Country to Allow ArgumentCompleter with $global:TfAcSbTwoLetterCountryCode

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
    Optional String. Proto-Name for the Routing Chain: Policy, PstnUsage and Voice Route.
    If provided will be deconstructed through other available particles.
    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 -Region AMER -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 -Region AMER -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 -Region AMER -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 -Region AMER -Country CA -Site Toronto -CallRestriction National -MatchingPattern "^\+1"
 
    Creates one route per Gateway, one PstnUsage and one Policy: OVP-AMER-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/main/docs/New-TeamsVoiceRoutingChain.md
  .LINK
    https://github.com/DEberhardt/TeamsFunctions/tree/main/docs/about_VoiceConfiguration.md
  .LINK
    https://github.com/DEberhardt/TeamsFunctions/tree/main/docs/
  #>


  [CmdletBinding(DefaultParameterSetName = 'Region', SupportsShouldProcess, ConfirmImpact = 'Medium')]
  [Alias('New-TeamsVRC')]
  [OutputType([System.Object])]
  param(
    [Parameter(HelpMessage = 'Proto Name for the Routing Chain')]
    #[ValidatePattern('^([A-z-_+])*[^\s]\1*$')] # Did not work with Call Restriction suffix attached?
    [ValidatePattern('^[a-zA-Z0-9-_]+$')]
    [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,})$)')]
    [ArgumentCompleter({ &$global:TfAcSbTeasmsPSTNGateway })]
    [String[]]$Gateway,

    [Parameter(Mandatory, ParameterSetName = 'Region', HelpMessage = 'Geo-Region to be added as a particle')]
    [Parameter(Mandatory, ParameterSetName = 'Unrestricted', HelpMessage = 'Geo-Region to be added as a particle')]
    [Parameter(Mandatory, ParameterSetName = 'Restricted', HelpMessage = 'Geo-Region to be added as a particle')]
    [Parameter(Mandatory, ParameterSetName = 'RestrictedCustom', HelpMessage = 'Geo-Region to be added as a particle')]
    [GeoRegion]$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(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 Live
    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'

    #region Country
    $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 -TwoLetterCode $Country }
          3 { Get-ISO3166Country -ThreeLetterCode $Country }
          default { $null }
        }
        if ( $CountryObject ) {
          $CountryCode = switch ( $Country.length ) {
            2 { $CountryObject.TwoLetterCode }
            3 { $CountryObject.ThreeLetterCode }
            default { $null }
          }
        }
        else {
          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
      }
    }
    #endregion

    #region Region
    $CurrentOperationID0 = 'Processing Region'
    Write-BetterProgress -Id 0 -Activity $ActivityID0 -Status $StatusID0 -CurrentOperation $CurrentOperationID0 -Step ($private:CountID0++) -Of $private:StepsID0
    if ( $PSBoundParameters.ContainsKey('Region') ) {
      if ($Region -ne $CountryObject.Region) {
        Write-Warning -Message "Determined Region '$($CountryObject.Region)' from Country and provided Region '$Region' do not match, this may be deliberate or an error."
      }
      [string]$VRCRegion = $Region
      Write-Information "INFO: Parameter Region provided. Using: '$VRCRegion'"
    }
    else {
      if ( $PSBoundParameters.ContainsKey('Country') ) {
        [string]$VRCRegion = $CountryObject.Region
        Write-Information "INFO: Site Region determined. Using: '$VRCRegion'"
      }
      else {
        Write-Error 'Region not determined. Please provide Region'
        continue
      }
    }
    #endregion

    #region Call Restriction
    $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'"
    }
    #endregion

    #region Matching Pattern
    $CurrentOperationID0 = 'Processing Matching Pattern'
    Write-BetterProgress -Id 0 -Activity $ActivityID0 -Status $StatusID0 -CurrentOperation $CurrentOperationID0 -Step ($private:CountID0++) -Of $private:StepsID0
    if ( $PSBoundParameters.ContainsKey('MatchingPattern') ) {
      if ( $CallRestriction -eq 'Unrestricted') {
        Write-Verbose -Message "MatchingPattern '$MatchingPattern' will be ignored. For Call Restriction '$CallRestriction' the Matching Pattern is Hardcoded." -Verbose
        $MatchingPattern = '.*'
        Write-Information "INFO: MatchingPattern used: '$MatchingPattern'"
      }
      else {
        Write-Information "INFO: Parameter MatchingPattern provided for Call Restriction '$CallRestriction' in Country '$Country': Using: '$MatchingPattern'"
      }
    }
    else {
      if ( $CallRestriction -eq 'Custom' -or $PSCmdlet.ParameterSetName -eq 'RestrictedCustom' ) {
        Write-Error -Message "Parameter MatchingPattern not provided, no data for Call Restriction '$CallRestriction'. Please provide MatchingPattern"
        continue
      }
      else {
        Write-Verbose -Message "MatchingPattern queried through 'Get-MatchingPatternForCallRestriction' for Country '$Country'"
        $CallRestrictionPatterns = Get-MatchingPatternForCallRestriction -Country $CountryCode
        $MatchingPattern = $CallRestrictionPatterns."$CallRestriction`Pattern"
        Write-Information "INFO: MatchingPattern not provided, for Call Restriction '$CallRestriction' in Country '$Country': Using: '$MatchingPattern'"
      }
    }
    #endregion
    #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

    $FormatVoiceRoutingChainInputParams = $null
    $FormatVoiceRoutingChainInputParams = @{
      Name   = $Name
      Region = $VRCRegion
    }
    if ( $PSBoundParameters.ContainsKey('Country') ) { $FormatVoiceRoutingChainInputParams += @{ Country = $CountryCode } }
    if ( $PSBoundParameters.ContainsKey('Site') ) { $FormatVoiceRoutingChainInputParams += @{ Site = $Site } }
    if ( $PSBoundParameters.ContainsKey('CallRestriction') ) { $FormatVoiceRoutingChainInputParams += @{ CallRestriction = $CallRestriction } }
    if ( $PSBoundParameters.ContainsKey('CustomRestrictionName') ) { $FormatVoiceRoutingChainInputParams += @{ CustomRestrictionName = $CustomRestrictionName } }
    $FormatVRCInput = Format-VoiceRoutingChainInput @FormatVoiceRoutingChainInputParams
    if ($PSBoundParameters.ContainsKey('Debug')) {
      " Function: $($MyInvocation.MyCommand.Name) - Voice Routing Chain Proto String:", ($FormatVRCInput | Format-Table -AutoSize | Out-String).Trim() | Write-Debug
    }

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

    $FormatVoiceRoutingChainOutputParams = $null
    $FormatVoiceRoutingChainOutputParams = @{
      Object = $FormatVRCInput
      Region = $VRCRegion
    }
    if ( $PSBoundParameters.ContainsKey('Country') ) { $FormatVoiceRoutingChainOutputParams += @{ Country = $CountryCode } }
    if ( $PSBoundParameters.ContainsKey('Site') ) { $FormatVoiceRoutingChainOutputParams += @{ Site = $Site } }
    if ( $PSBoundParameters.ContainsKey('CallRestriction') ) { $FormatVoiceRoutingChainOutputParams += @{ CallRestriction = $CallRestriction } }
    if ( $PSBoundParameters.ContainsKey('CustomRestrictionName') ) { $FormatVoiceRoutingChainOutputParams += @{ CustomRestrictionName = $CustomRestrictionName } }
    [string]$ProtoString = Format-VoiceRoutingChainOutput @FormatVoiceRoutingChainOutputParams

    # Constructing Names
    Write-Information "INFO: Common String used across OVP, OPU & OVR: '$ProtoString' - This will be pre-fixed depending on the artifact"
    $PolicyName = 'OVP-' + $ProtoString
    $UsageName = 'OPU-' + $ProtoString
    $RouteName = 'OVR-' + $ProtoString
    Write-Verbose -Message "OVP Name: '$PolicyName'" -Verbose
    Write-Verbose -Message "OPU Name: '$UsageName'" -Verbose
    Write-Verbose -Message "OVR Name: '$RouteName' (suffix will be attached if multiple Gateways are specified)" -Verbose
    #endregion
  }

  Process {
    # Initialising Objects
    [System.Collections.Generic.List[object]]$Usages = @()
    [System.Collections.Generic.List[object]]$Routes = @()
    [System.Collections.Generic.List[object]]$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 ($VRCRegion) { $VRCRegion + ' - ' }) + $(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 $Usages) {
        if ($PSBoundParameters.ContainsKey('Debug')) {
          " Function: $($MyInvocation.MyCommand.Name) - OPU:", ($OPU | Format-Table -AutoSize | Out-String).Trim() | Write-Debug
        }
        try {
          $null = Set-CsOnlinePstnUsage Global -Usage @{ add = "$OPU" } -ErrorAction Stop
          Write-Information "INFO: Online Pstn Usage '$OPU' created"
        }
        catch {
          if ($_.Exception.Message.Contains('duplicate key')) {
            Write-Warning -Message "Online Pstn Usage '$OPU' 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
    foreach ($OVR in $Routes) {
      if ($PSCmdlet.ShouldProcess("$($OVR.Name)", 'New-CsOnlineVoiceRoute')) {
        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 '$($OVR.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