Public/Start-NSPEntraPairingUpnSuffixReview.ps1

function Start-NSPEntraPairingUpnSuffixReview {
    <#
    .SYNOPSIS
        Reviews verified cloud domains missing as AD UPN suffixes, with explicit selected additions.
    .DESCRIPTION
        Default is review/preview. Apply enables an explicit add action after showing the AD plan;
        confirmation is required for that forest change. The AD module owns planning and writing.
        Every addition refreshes the domain/forest comparison. No users or cloud domains change.
    .EXAMPLE
        Start-NSPEntraPairingUpnSuffixReview -ExpectedTenantId $queue.tenantId -ExpectedForest $queue.forest -Server $server
    .EXAMPLE
        Start-NSPEntraPairingUpnSuffixReview -ExpectedTenantId $queue.tenantId -ExpectedForest $queue.forest -Server $server -Apply
    #>

    [CmdletBinding(SupportsShouldProcess, ConfirmImpact='High')]
    param([Parameter(Mandatory)][guid]$ExpectedTenantId,
        [Parameter(Mandatory)][string]$ExpectedForest, [Parameter(Mandatory)][string]$Server,
        [switch]$Apply)
    Import-NSPToolkitModule -Name 'NSP.ActiveDirectory'
    Import-NSPToolkitModule -Name 'NSP.Console' -MinimumVersion '0.1.1'
    foreach ($command in 'Get-NSPADForestInfo','Get-NSPADForestUPNSuffixPlan','Add-NSPADForestUPNSuffix') {
        if (-not (Get-Command $command -ErrorAction SilentlyContinue)) { throw 'The AD forest/suffix APIs are not available yet. Install the AD specialist delivery before using this gate.' }
    }
    $results = [System.Collections.Generic.List[object]]::new()
    $failure = $null
    try {
    while ($true) {
        $forest = Get-NSPADForestInfo -Server $Server
        if ([string]$forest.ForestDNSRoot -ine $ExpectedForest) { throw 'The AD forest differs from the expected forest.' }
        if ($null -eq $forest.PSObject.Properties['AvailableUPNSuffixes']) { throw 'AD did not return available UPN suffixes; availability is unknown.' }
        $comparison = @(Get-NSPEntraPairingUpnSuffixComparison -ExpectedTenantId $ExpectedTenantId -Forest $ExpectedForest -AvailableSuffix @($forest.AvailableUPNSuffixes))
        foreach ($item in $comparison) { Write-NSPConsoleLine -Message "$($item.Suffix) [$($item.Status)]" }
        $missing = @($comparison | Where-Object Status -eq Missing)
        if (-not $missing.Count) { break }
        $choices = @($missing | ForEach-Object { [pscustomobject]@{ Label="Review missing suffix $($_.Suffix)"; Value=$_.Suffix } })
        $suffix = Read-NSPMenu -Prompt 'Choose a missing verified domain to preview; nothing is added automatically' -Choices $choices -CancelLabel 'Back to user preparation'
        if ($null -eq $suffix) { break }
        if ($suffix -notin @($missing.Suffix)) { throw 'Selected suffix was not in the missing verified-domain list.' }
        $plan = Get-NSPADForestUPNSuffixPlan -ForestInfo $forest -Suffix $suffix
        Write-NSPConsoleLine -Message ($plan | ConvertTo-Json -Depth 8)
        if (-not $Apply -or $plan.Blocked -or -not $plan.Changed) { continue }
        $action = Read-NSPMenu -Prompt 'Review the additive forest change above' -Choices @(
            [pscustomobject]@{Label='Return without adding';Value='back'}
            [pscustomobject]@{Label="Add only $suffix to $ExpectedForest";Value='add'}
        )
        if ($action -ne 'add') { continue }
        # Check Graph again just before applying; the AD writer independently rejects stale forest plans.
        $current = @(Get-NSPEntraPairingUpnSuffixComparison -ExpectedTenantId $ExpectedTenantId -Forest $ExpectedForest -AvailableSuffix @($forest.AvailableUPNSuffixes))
        if ($suffix -notin @($current.Suffix)) { throw 'The suffix is no longer a verified domain in this tenant.' }
        if ($PSCmdlet.ShouldProcess("$ExpectedForest via $($forest.Server)", "Add UPN suffix $suffix")) {
            $result = Add-NSPADForestUPNSuffix -Plan $plan -Apply -Confirm:$false
            $results.Add($result)
            Write-NSPConsoleLine -Message ($result | ConvertTo-Json -Depth 8)
            if ($result.Result -notin @('Verified','AlreadyAvailable')) { break }
        }
    }
    } catch {
        if (-not $results.Count) { throw }
        $failure = $_.Exception.Message
    }
    [pscustomobject]@{ Comparison=$comparison; Results=$results.ToArray(); Error=$failure }
}