Public/Invoke-NSPEntraPairingAddressPreparation.ps1

function Invoke-NSPEntraPairingAddressPreparation {
    <#
    .SYNOPSIS
        Previews AD preparation, or explicitly applies it and invalidates the pairing queue first.
    .DESCRIPTION
        Uses the AD specialist's planner and writer. Apply requires a saved queue path and retains
        results there as addressPreparation. Any apply attempt requires fresh inventories and queue
        revalidation before hard match, even if a write fails. No synchronization is started.
    .EXAMPLE
        Invoke-NSPEntraPairingAddressPreparation -Queue $queue -Inventory $inventory -OldDomain old.example -NewDomain new.example
    .EXAMPLE
        Invoke-NSPEntraPairingAddressPreparation -Queue $queue -Inventory $inventory -OldDomain old.example -NewDomain new.example -Apply -Path $path
    #>

    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')]
    param([Parameter(Mandatory)]$Queue, [Parameter(Mandatory)]$Inventory,
        [Parameter(Mandatory)][string]$OldDomain, [Parameter(Mandatory)][string]$NewDomain,
        [ValidateSet('ReplaceSuffix','SamAccountName','FLast','First.Last','LastF')][string]$Format = 'ReplaceSuffix',
        [switch]$Apply, [string]$Path)
    $plans = @(Get-NSPEntraPairingAddressPlan -Queue $Queue -Inventory $Inventory -OldDomain $OldDomain -NewDomain $NewDomain -Format $Format)
    if (-not $Apply -or $WhatIfPreference) {
        if ($Apply) { [void]$PSCmdlet.ShouldProcess('selected AD users', 'Apply reviewed address preparation') }
        $plans
        return
    }
    if (-not $plans.Count) { return }
    if (@($plans | Where-Object Blocked).Count) { throw 'Address preparation is blocked; review the plan reasons.' }
    if (-not $Path) { throw 'Apply requires -Path for saving queue invalidation and results.' }
    Assert-NSPEntraPairingStatePath -Path $Path
    if (-not $PSCmdlet.ShouldProcess('selected AD users', 'Apply reviewed address preparation; hard match remains separate')) { return }
    foreach ($pair in @($Queue.pairs | Where-Object decision -eq 'selected')) {
        if ($pair.executionState -ne 'pending') { throw 'An attempted hard match must be reconciled before AD address preparation.' }
    }
    $Queue.needsRevalidation = $true
    $Queue.revalidatedAtUtc = $null
    foreach ($pair in @($Queue.pairs | Where-Object decision -eq 'selected')) {
        $pair.history = @($pair.history) + [pscustomobject]@{ decision=$pair.decision; adObjectGuid=$pair.adObjectGuid; decidedAtUtc=$pair.decidedAtUtc; decidedBy=$pair.decidedBy; note=$pair.note }
        $pair.decision = 'needs-review'
        $pair.desiredImmutableId = $null
    }
    $state = [pscustomobject]@{ StartedAtUtc = [datetime]::UtcNow.ToString('o'); Outcome = 'started'; Results = @(); Error = $null }
    $Queue | Add-Member -NotePropertyName addressPreparation -NotePropertyValue $state -Force
    Export-NSPEntraPairingQueue -Queue $Queue -Path $Path | Out-Null
    try {
        Set-NSPADUserAddress -Plan $plans -Apply | ForEach-Object {
            $state.Results = @($state.Results) + $_
            Export-NSPEntraPairingQueue -Queue $Queue -Path $Path | Out-Null
        }
        $state.Outcome = if (@($state.Results | Where-Object { $_.Result -in @('Failed','WriteUnverified','VerificationFailed') }).Count) { 'needs-inspection' } else { 'requires-refresh' }
    } catch {
        $state.Outcome = 'unknown'
        $state.Error = $_.Exception.Message
    }
    Export-NSPEntraPairingQueue -Queue $Queue -Path $Path | Out-Null
    $state
}