Public/AD/Get-ADUsersToUpdate.ps1

<#
.SYNOPSIS
Compute the AD update, rename, and move lists for linked, active users.
 
.DESCRIPTION
For each active, AD-provisioned source user linked to an AD account, diffs the desired state
against the current AD user and produces three lists: an UpdateList of Set-ADUser splats for
changed attributes (username/UPN, EmployeeID/Number, name fields, office/title/company/department,
optional Description/OfficePhone/Email, passwordNeverExpires, enabled state including ForceDisable,
and EmployeeType/extensionAttributes); a RenameList when the CN differs from "First Last PersonID";
and a MoveList when the user is in the wrong OU. Name comparisons are case-sensitive so a casing
fix from the plugin is applied. A username change that collides with a different existing account
is logged and skipped.
 
.PARAMETER UserList
The enriched source records.
 
.PARAMETER LookupByID
The AD LookupByID hashtable (EmployeeID -> AD user) from Get-TargetDataAD.
 
.OUTPUTS
[pscustomobject] @{ UpdateList; RenameList; MoveList }.
 
.EXAMPLE
$toUpdate = Get-ADUsersToUpdate -UserList $sourceData -LookupByID $adData.LookupByID
 
.NOTES
   Created by: Sam Cattanach
   Modified: 2026-06-26
#>

function Get-ADUsersToUpdate {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        $UserList,

        [Parameter(Mandatory = $true)]
        $LookupByID
    )

    $itemUpdateList = @()
    $itemRenameList = @()
    $itemMoveList = @()

    foreach ($item in $UserList | Where-Object {$_.IDBActive -eq $true -and $_.ProvisionAD -eq $true -and $_.ADCurrentUserID}) {
        $ADUser = $null
        $ADUser = $LookupByID[$item.personID]

        $itemUpdateSplat = @{}

        if ($ADUser.SamAccountName -ne $item.Username) {
            try {
                Get-ADUser -Identity $item.UPN -ErrorAction Stop | Out-Null

                Write-Log -Message ("AD: Another user account has the username of " + $item.Username + ". Terminating updating person: " + $item.PersonID) -Level Error

                continue
            }
            catch {
                if ($_.CategoryInfo.Reason -eq 'ADIdentityNotFoundException') {
                    Write-Log -Message ("AD: New Username found for " + $item.PersonID + ". Old username is " + $ADUser.SamAccountName + ". New username is " + $item.Username + ".")

                    $itemUpdateSplat["SamAccountName"] = $item.Username
                    $itemUpdateSplat["UserPrincipalName"] = $item.UPN
                }
            }
        }

        if ($ADUser.EmployeeID -ne $item.PersonID) {
            $itemUpdateSplat["EmployeeID"] = $item.PersonID
        }

        if ($item.InternalID -and $ADUser.EmployeeNumber -ne $item.InternalID) {
            $itemUpdateSplat["EmployeeNumber"] = $item.InternalID
        }

        # Names compare case-sensitively (-cne) so a casing fix from the plugin (e.g. ALL-CAPS ->
        # Title-Case) is applied, not just content changes.
        if ($ADUser.Surname -cne $item.NameLast.trim()) {
            $itemUpdateSplat["Surname"] = $item.NameLast.trim()
        }

        if ($ADUser.GivenName -cne $item.NameFirst.trim()) {
            $itemUpdateSplat["GivenName"] = $item.NameFirst.trim()
        }

        if ($ADUser.DisplayName -cne ($item.NameFirst.trim() + " " + $item.NameLast.trim())) {
            $itemUpdateSplat["DisplayName"] = ($item.NameFirst.trim() + " " + $item.NameLast.trim())
        }

        if ($ADUser.physicalDeliveryOfficeName -ne $item.Building) {
            $itemUpdateSplat["Office"] = $item.Building
        }

        if ($ADUser.title -ne $item.JobTitle) {
            $itemUpdateSplat["Title"] = $item.JobTitle
        }

        if ($ADUser.company -ne $item.company) {
            $itemUpdateSplat["Company"] = $item.company
        }

        if ($ADUser.Department -ne $item.Department) {
            $itemUpdateSplat["Department"] = $item.Department
        }

        #Optional attributes - set-but-don't-clear (only push when the record provides a value)
        if ($item.Description -and $ADUser.Description -ne $item.Description) {
            $itemUpdateSplat["Description"] = $item.Description
        }

        if ($item.TelephoneNumber -and $ADUser.OfficePhone -ne $item.TelephoneNumber) {
            $itemUpdateSplat["OfficePhone"] = $item.TelephoneNumber
        }

        if ($item.EmailAddress -and $ADUser.EmailAddress -ne $item.EmailAddress) {
            $itemUpdateSplat["EmailAddress"] = $item.EmailAddress
        }

        if ($ADUser.PasswordNeverExpires -ne $item.PasswordNeverExpires) {
            $itemUpdateSplat["PasswordNeverExpires"] = $item.PasswordNeverExpires
        }

        if ($ADUser.Enabled -ne $true) {
            $itemUpdateSplat["Enabled"] = $true
        }

        if ($item.ForceDisable -eq "TRUE") {
            $itemUpdateSplat["Enabled"] = $false
        }

        $replace = @{}
        if ($ADUser.EmployeeType -ne $item.PersonTypeID -or $ADUser.extensionAttribute1 -ne $item.PersonTypeID) {
            $replace['EmployeeType'] = $item.PersonTypeID
            $replace['extensionAttribute1'] = $item.PersonTypeID
        }
        if ($item.ExtensionAttribute2 -and $ADUser.extensionAttribute2 -ne $item.ExtensionAttribute2) { $replace['extensionAttribute2'] = $item.ExtensionAttribute2 }
        if ($item.ExtensionAttribute3 -and $ADUser.extensionAttribute3 -ne $item.ExtensionAttribute3) { $replace['extensionAttribute3'] = $item.ExtensionAttribute3 }
        if ($item.ExtensionAttribute4 -and $ADUser.extensionAttribute4 -ne $item.ExtensionAttribute4) { $replace['extensionAttribute4'] = $item.ExtensionAttribute4 }
        if ($replace.Count -gt 0) {
            $itemUpdateSplat["Replace"] = $replace
        }

        if ($itemUpdateSplat.Count -gt 0) {
            $itemUpdateSplat["Identity"] = $item.ADCurrentUserID
            $itemUpdateSplat["Division"] = (Get-Date -format yyyy-MM-dd-HH:mm)

            Write-Log -Message ("AD: Information that needs updating for: " + $item.UPN + " - " + $item.personID)
            Write-Log -Message ($itemUpdateSplat | ConvertTo-Json -Compress)

            $itemUpdateList += [PSCustomObject]@{
                CN = $ADUser.CN
                Splat = $itemUpdateSplat
            }
        }

        if ($ADUser.CN -cne ($item.NameFirst.trim() + " " + $item.NameLast.trim() + " " + $item.PersonID)) {
            Write-Log -Message ("AD: Canonical Name does not match for " + $item.PersonID + ".")

            $itemRenameList += [PSCustomObject]@{
                CN = $ADUser.CN
                ADUserID = $item.ADCurrentUserID
                NewName = "$($item.NameFirst.trim()) $($item.NameLast.trim()) $($item.PersonID)"
            }
        }

        if ($ADUser.DistinguishedName.split(",",2)[1] -ne $item.ADOrganizationalUnit) {
            Write-Log -Message ("AD: Organization Unit does not match for " + $item.PersonID + ".")

            $itemMoveList += [PSCustomObject]@{
                CN = $ADUser.CN
                ADUserID = $item.ADCurrentUserID
                NewOrgUnit = $item.ADOrganizationalUnit
            }
        }
    }

    return [PSCustomObject]@{
        UpdateList = $itemUpdateList
        RenameList = $itemRenameList
        MoveList = $itemMoveList
    }
}