Remediation/TeamsPhone/TEL-DIAL-PLANS.ps1

# UNTESTED: this script has never been run on a real tenant. Read it, try it on a test tenant first, and use it at your own risk.
<#
.SYNOPSIS
    Recreates the voice routing specific to the sources and assigns the routing policy to voice users (procedure TEL-DIAL-PLANS).
 
.DESCRIPTION
    The script tells what is specific to a source from what each tenant gets by default: the Global plan with no rule, the Global policy
    with no usage and the LocalRoute route with no gateway are not recreated. What stays specific to a source becomes an instruction,
    with its name conflict check in the target.
    The routing policy only serves direct routing. It depends on TEL-ROUTING-TARGET:
      - type DirectRouting: decisions\TEL-DIAL-PLANS.csv names the policy of the target to use;
      - type OperatorConnect or CallingPlan: no policy to assign.
    The Global dial plan of the target already applies to all its accounts: no assignment per user.
    Only accounts already in the target according to the snapshot get the policy: run again after a new snapshot.
#>

$c = Open-TcRemediation

$TypeFile = 'TEL-ROUTING-TARGET'
$targetType = if (Test-TcDecisionFile $TypeFile) { @(Read-TcDecisionRow $TypeFile | ForEach-Object { (Get-TcMember $_ 'Decision') } | Where-Object { $_ }) | Select-Object -First 1 }

$targetAccounts = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase)
foreach ($u in Get-TcRemediationTable 'Target' 'teamsUsers') { [void]$targetAccounts.Add([string]$u['UPN']) }
$targetNames = @(foreach ($n in 'dialPlans', 'voiceRoutes', 'voiceRoutingPolicies') { Get-TcRemediationTable 'Target' $n | ForEach-Object { [string]$_['Name'] } })

$policy = ''
if ($targetType -eq 'DirectRouting') {
    $proposals = @(Get-TcRemediationTable 'Target' 'voiceRoutingPolicies' | Where-Object { $_['Name'] -ne 'Global' } | ForEach-Object { [ordered]@{ 'Policy' = $_['Name']; 'Usages' = $_['PSTN usages']; Proposal = '' } })
    $chosenPolicies = @(Get-TcDecision -Name 'TEL-DIAL-PLANS' -Proposal $proposals -Key 'Policy' -Choice 'Keep', 'Ignore' | Where-Object { (Get-TcMember $_ 'Decision') -eq 'Keep' })
    if ($chosenPolicies.Count -gt 1) { Write-TcRemediationAlert ("{0} policies chosen: only one is expected, no assignment planned" -f $chosenPolicies.Count) }
    elseif ($chosenPolicies.Count -eq 1) { $policy = (Get-TcMember $chosenPolicies[0] 'Policy') -replace '^Tag:', '' }
}

foreach ($t in $c.Tenants) {
    Write-TcRemediationTenant $t
    $plans = @(Get-TcRemediationTable $t 'dialPlans' | Where-Object { $_['Name'] -ne 'Global' -or [int]$_['Rules'] -gt 0 })
    $routes = @(Get-TcRemediationTable $t 'voiceRoutes' | Where-Object { $_['Gateways'] -or $_['Usages'] })
    $policies = @(Get-TcRemediationTable $t 'voiceRoutingPolicies' | Where-Object { $_['Name'] -ne 'Global' -or $_['PSTN usages'] })
    $people = @(Get-TcRemediationTable $t 'teamsUsers' | Where-Object { (ConvertTo-TcBoolean $_['Enterprise Voice']) -and $_['Features'] -notmatch 'VoiceApp' })
    Write-TcRemediationNote ("Specific to the source: {0} dial plan(s), {1} route(s), {2} policy(ies); {3} voice user(s)" -f $plans.Count, $routes.Count, $policies.Count, $people.Count)

    foreach ($x in @($plans + $routes + $policies)) {
        $conflict = if ([string]$x['Name'] -in $targetNames) { ' This name already exists in the target: choose another.' } else { '' }
        Add-TcManualAction -Tenant 'Target' -Object ([string]$x['Name']) -Description ("Recreate in the target the voice element {0} of {1}" -f $x['Name'], $t) `
            -Instruction @(("Element specific to {0}: {1}. The snapshot does not carry its rules: read them in {0}, then recreate them in the target.{2}" -f $t, ($x | ConvertTo-Json -Compress), $conflict), 'Check the order of the routes: a misplaced route sends calls to the wrong gateway.')
    }

    if (-not $people.Count) { continue }
    if (-not $targetType) { Write-TcRemediationAlert 'Number type not decided (TEL-ROUTING-TARGET): no assignment planned.'; continue }
    if ($targetType -ne 'DirectRouting') { Write-TcRemediationNote ("Type {0}: the routing policy does not apply" -f $targetType); continue }
    if (-not $policy) { Write-TcRemediationAlert 'Routing policy not chosen in decisions\TEL-DIAL-PLANS.csv: no assignment planned.'; continue }

    $matrix = @{}
    foreach ($l in @((Get-TcMember (Get-TcRemediationIdentity $t) 'rows'))) { $matrix[((Get-TcText $l 'Source UPN')).ToLowerInvariant()] = $l }
    $missing = 0
    foreach ($u in $people) {
        $l = $matrix[([string]$u['UPN']).ToLowerInvariant()]
        if ($l -and (Get-TcMember $l 'Decision') -eq 'Exclude') { continue }
        $target = if ($l) { @((Get-TcMember $l 'Target UPN'), (Get-TcMember $l 'Temporary UPN') | Where-Object { $_ -and $targetAccounts.Contains([string]$_) }) | Select-Object -First 1 }
        if (-not $target) { $missing++; continue }
        Add-TcCommandAction -Tenant 'Target' -Service Teams -Command 'Grant-CsOnlineVoiceRoutingPolicy' -Parameters @{ Identity = [string]$target; PolicyName = $policy } -Object ([string]$target) `
            -Description ("Assign the routing policy {0} to {1}" -f $policy, $target) `
            -Test {
                param($a)
                $o = Invoke-TcServiceCommand -Tenant $a.tenant -Service Teams -Command 'Get-CsOnlineUser' -Parameters @{ Identity = $a.parameters.Identity }
                $before = (Get-TcText $o 'OnlineVoiceRoutingPolicy')
                $a.undo = @{ command = 'Grant-CsOnlineVoiceRoutingPolicy'; parameters = @{ Identity = $a.parameters.Identity; PolicyName = $(if ($before) { $before }) } }
                return $before -eq $a.parameters.PolicyName
            }
    }
    if ($missing) { Write-TcRemediationAlert ("{0} account(s) not in the target yet: run again after they are created and after a new snapshot" -f $missing) }
}

Invoke-TcRemediationPlan -Verify {
    param($t)
    if ($t -ne $c.Tenants[0]) { return }
    $routes = @(Invoke-TcServiceCommand -Tenant 'Target' -Service Teams -Command 'Get-CsOnlineVoiceRoute')
    $policies = @(Invoke-TcServiceCommand -Tenant 'Target' -Service Teams -Command 'Get-CsOnlineVoiceRoutingPolicy')
    New-TcRemediationResult Info ("Target: {0} route(s), {1} routing policy(ies)" -f $routes.Count, $policies.Count)
    if ($policy) {
        if (@($policies | Where-Object { ((Get-TcText $_ 'Identity') -replace '^Tag:', '') -eq $policy }).Count) { New-TcRemediationResult OK "policy $policy present" } else { New-TcRemediationResult Blocking "policy $policy not found in the target" }
    }
}
Close-TcRemediation