Public/Add-NSPEntraPairDecision.ps1

function Add-NSPEntraPairDecision {
    <#
    .SYNOPSIS
        Records one operator decision in a queue, and keeps the earlier ones. Offline.
    .DESCRIPTION
        selected names an AD user: one of the pair's candidates with -AdObjectGuid, or a user from
        a search with -ADUser, whose domain must be one of the queue's. It is refused on a loaded queue
        that has not been revalidated, for accounts that are diagnostic only, and for an AD user
        already selected elsewhere in the queue or already linked, because one AD user must never map
        to several cloud accounts. skipped, flagged and deferred clear any selection.
 
        Once a write has been attempted the decision is closed: the cloud user may hold the anchor,
        and Sync-NSPEntraPairingQueue reopens the pair if it does not.
 
        The desired immutable ID is filled only under a verified source anchor. Without one, the
        decision is still recorded, and the pair cannot be written.
    .EXAMPLE
        Add-NSPEntraPairDecision -Queue $queue -CloudObjectId $id -Decision selected -AdObjectGuid $guid
    .EXAMPLE
        Add-NSPEntraPairDecision -Queue $queue -CloudObjectId $id -Decision flagged -Note 'Shared account?'
    #>

    [CmdletBinding(DefaultParameterSetName = 'NoSelection')]
    param(
        [Parameter(Mandatory)]$Queue,
        [Parameter(Mandatory)][string]$CloudObjectId,
        [Parameter(Mandatory)][ValidateSet('selected', 'skipped', 'flagged', 'deferred')][string]$Decision,
        [Parameter(Mandatory, ParameterSetName = 'Candidate')][guid]$AdObjectGuid,
        [Parameter(Mandatory, ParameterSetName = 'Search')]$ADUser,
        [string]$Note
    )

    $pair = @($Queue.pairs | Where-Object cloudObjectId -eq $CloudObjectId)
    if ($pair.Count -ne 1) { throw "The queue has $($pair.Count) pairs for cloud object $CloudObjectId." }
    $pair = $pair[0]

    if ($pair.executionState -in @('writing', 'applied', 'unconfirmed', 'different', 'unknown', 'linked')) {
        throw "This pair's write is $($pair.executionState), so its decision cannot change. Run Sync-NSPEntraPairingQueue to read what the cloud user holds."
    }

    $previous = [pscustomobject]@{ decision = $pair.decision; adObjectGuid = $pair.adObjectGuid; decidedAtUtc = $pair.decidedAtUtc; decidedBy = $pair.decidedBy; note = $pair.note }
    $selection = @{ adObjectGuid = $null; adSid = $null; dn = $null; upn = $null; enabled = $null; evidence = @(); immutableId = $null }

    if ($Decision -eq 'selected') {
        if ($PSCmdlet.ParameterSetName -eq 'NoSelection') { throw 'selected needs -AdObjectGuid or -ADUser.' }
        if ($Queue.needsRevalidation) { throw 'This queue was loaded and has not been revalidated. Run Sync-NSPEntraPairingQueue first.' }
        if ($pair.status -notin @('Suggested', 'Ambiguous', 'OneToMany', 'NotFound')) {
            throw "This pair is $($pair.status), which is diagnostic only. It cannot be selected."
        }
        if ($PSCmdlet.ParameterSetName -eq 'Candidate') {
            $chosen = @($pair.candidates | Where-Object adObjectGuid -eq ([string]$AdObjectGuid))
            if ($chosen.Count -ne 1) { throw "AD user $AdObjectGuid is not a candidate for this pair. Pass it with -ADUser after a search." }
            $selection = @{ adObjectGuid = $chosen[0].adObjectGuid; adSid = $chosen[0].adSid; dn = $chosen[0].distinguishedName; upn = $chosen[0].userPrincipalName; enabled = $chosen[0].enabled; evidence = @($chosen[0].evidence); immutableId = $chosen[0].desiredImmutableId }
        } else {
            if ([string]$ADUser.DomainDNSRoot -notin @($Queue.domains)) {
                throw "AD user $($ADUser.ObjectGUID) is in domain $($ADUser.DomainDNSRoot), which is not in this queue."
            }
            $immutableId = $null
            if ($Queue.sourceAnchor.status -eq 'verified') { $immutableId = ConvertTo-NSPEntraImmutableId -ADUser $ADUser -Strategy $Queue.sourceAnchor.strategy }
            $selection = @{ adObjectGuid = [string]$ADUser.ObjectGUID; adSid = [string]$ADUser.ObjectSID; dn = [string]$ADUser.DistinguishedName; upn = [string]$ADUser.UserPrincipalName; enabled = [bool]$ADUser.Enabled; evidence = @('operator-search'); immutableId = $immutableId }
        }
        $target = $selection.adObjectGuid
        if ($target -in @($Queue.linkedAdObjectGuids)) { throw "AD user $target is already linked to a cloud account." }
        $other = @($Queue.pairs | Where-Object { $_.decision -eq 'selected' -and $_.adObjectGuid -eq $target -and $_.cloudObjectId -ne $CloudObjectId })
        if ($other.Count) { throw "AD user $target is already selected for cloud object $($other[0].cloudObjectId)." }
    }

    $pair.history = @($pair.history) + $previous
    $pair.decision = $Decision
    $pair.adObjectGuid = $selection.adObjectGuid
    $pair.adSid = $selection.adSid
    $pair.adDistinguishedNameAtReview = $selection.dn
    $pair.adUserPrincipalNameAtReview = $selection.upn
    $pair.adEnabledAtReview = $selection.enabled
    $pair.matchEvidence = @($selection.evidence)
    $pair.desiredImmutableId = $selection.immutableId
    $pair.decidedAtUtc = [datetime]::UtcNow.ToString('o')
    $pair.decidedBy = [Environment]::UserName
    $pair.note = $Note
    $pair
}