public/Update-SwSdIncident.ps1

function Update-SwSdIncident {
    <#
    .SYNOPSIS
        Updates the specified incident record with the provided assignee and/or status.
    .DESCRIPTION
        Updates the specified incident record with the provided assignee and/or status.
        You can specify either the assignee or status, or both.
        Assignee must be a valid SWSD user account.
    .PARAMETER Number
        The incident number.
    .PARAMETER Assignee
        The email address of the assignee.
    .PARAMETER GroupAssignee
        The name of the group assignee. This parameter is used to assign the incident to a group instead of an individual user. The group name must be a valid SWSD group.
    .PARAMETER GroupAssigneeId
        The numeric ID of the group/queue assignee. Use this when queue names do not map to a Group name.
    .PARAMETER Status
        The status of the incident: Awaiting Input, Assigned, Closed, On Hold, Pending Assignment, Scheduled.
        The default status is 'Assigned'.
    .PARAMETER Description
        The description of the incident update. This parameter is used to add a comment to the incident when updating the assignee or status. The description will be added as a comment to the incident.
    .PARAMETER Category
        The category of the incident. This parameter is used to update the incident category. The category must be a valid SWSD category.
    .EXAMPLE
        Update-SwSdIncident -Number 12345 -Assignee "jsmith@contoso.org" -Status "Pending Assignment"
        
        Updates the incident 12345 with the specified assignee 'jsmith@contoso.org' and status 'Pending Assignment'.
    .EXAMPLE
        Update-SwSdIncident -Number 12345 -Status "Closed"

        Updates the incident 12345 with the specified status 'Closed'
    .NOTES
        The Assignee must be a valid SWSD user account.
        Reference: https://apidoc.samanage.com/#tag/Incident/operation/updateIncidentById
    .LINK
        https://github.com/Skatterbrainz/SolarWinds.ServiceDesk/blob/main/docs/Update-SwSdIncident.md
    #>

    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param (
        [parameter(Mandatory = $True)][string][ValidateNotNullOrWhiteSpace()]$Number,
        [parameter(Mandatory = $False)][string][Alias('Email')]$Assignee,
        [parameter(Mandatory = $False)][string][Alias('Group')]$GroupAssignee,
        [parameter(Mandatory = $False)][int][Alias('GroupId', 'QueueId')]$GroupAssigneeId,
        [parameter(Mandatory = $False)][string][Alias('State')]$Status,
        [parameter(Mandatory = $False)][string]$Category,
        [parameter(Mandatory = $False)][string]$SubCategory,
        [parameter(Mandatory = $False)][string]$Description
    )
    try {
        $SDSession = Connect-SwSD

        function Invoke-IncidentUpdateRequest {
            param(
                [parameter(Mandatory = $True)][string]$Uri,
                [parameter(Mandatory = $True)][hashtable]$Headers,
                [parameter(Mandatory = $True)][hashtable]$IncidentBody
            )
            $payload = @{ incident = $IncidentBody } | ConvertTo-Json -Depth 10
            $params = @{
                Method          = "PUT"
                Uri             = $Uri
                ContentType     = "application/json"
                Headers         = $Headers
                Body            = $payload
                UseBasicParsing = $true
            }
            Write-Verbose "Request body: $(@{ incident = $IncidentBody } | ConvertTo-Json -Depth 10)"
            try {
                $response = Invoke-WebRequest @params
                return [pscustomobject]@{
                    Success    = $true
                    StatusCode = [int]$response.StatusCode
                    ErrorBody  = $null
                }
            } catch {
                $statusCode = $null
                $errorBody = $null
                if ($_.Exception.Response) {
                    try {
                        $statusCode = [int]$_.Exception.Response.StatusCode
                    } catch {
                    }
                }
                if ($_.ErrorDetails -and $_.ErrorDetails.Message) {
                    $errorBody = $_.ErrorDetails.Message
                }
                return [pscustomobject]@{
                    Success    = $false
                    StatusCode = $statusCode
                    ErrorBody  = $errorBody
                }
            }
        }

        function Get-IncidentByHref {
            param(
                [parameter(Mandatory = $True)][string]$Uri,
                [parameter(Mandatory = $True)][hashtable]$Headers
            )
            $requestUri = $Uri
            if ($requestUri -notmatch '\?') {
                $requestUri = "$requestUri?layout=long"
            } elseif ($requestUri -notmatch '(^|&)layout=') {
                $requestUri = "$requestUri&layout=long"
            }
            $params = @{
                Method          = "GET"
                Uri             = $requestUri
                Headers         = $Headers
                ContentType     = "application/json"
                UseBasicParsing = $true
            }
            $response = Invoke-WebRequest @params
            $data = $response.Content | ConvertFrom-Json
            if ($data -and $data.incident) { return $data.incident }
            return $data
        }

        function Test-QueueAssignmentMatch {
            param(
                [parameter(Mandatory = $True)]$IncidentObject,
                [parameter(Mandatory = $False)][int]$QueueId,
                [parameter(Mandatory = $False)][string]$QueueName
            )
            $groupId = if ($IncidentObject.group_assignee -and $IncidentObject.group_assignee.id) { [string]$IncidentObject.group_assignee.id } else { $null }
            $groupName = if ($IncidentObject.group_assignee -and $IncidentObject.group_assignee.name) { [string]$IncidentObject.group_assignee.name } else { $null }
            $assigneeIsUser = $null
            if ($IncidentObject.assignee -and $null -ne $IncidentObject.assignee.is_user) {
                $assigneeIsUser = [string]$IncidentObject.assignee.is_user
            }
            $assigneeId = if ($IncidentObject.assignee -and $IncidentObject.assignee.id) { [string]$IncidentObject.assignee.id } else { $null }
            $assigneeName = if ($IncidentObject.assignee -and $IncidentObject.assignee.name) { [string]$IncidentObject.assignee.name } else { $null }

            $matched = $false
            if ($QueueId) {
                $matched = ($groupId -eq [string]$QueueId) -or (($assigneeIsUser -eq 'False' -or $assigneeIsUser -eq 'false') -and ($assigneeId -eq [string]$QueueId))
            } elseif (![string]::IsNullOrWhiteSpace($QueueName)) {
                $matched = ($groupName -ieq [string]$QueueName) -or (($assigneeIsUser -eq 'False' -or $assigneeIsUser -eq 'false') -and ($assigneeName -ieq [string]$QueueName))
            }

            return [pscustomobject]@{
                Matched      = $matched
                GroupId      = $groupId
                GroupName    = $groupName
                AssigneeId   = $assigneeId
                AssigneeName = $assigneeName
                AssigneeUser = $assigneeIsUser
            }
        }

        if ([string]::IsNullOrEmpty($Assignee) -and [string]::IsNullOrEmpty($GroupAssignee) -and !$GroupAssigneeId -and [string]::IsNullOrEmpty($Status) -and [string]::IsNullOrEmpty($Category) -and [string]::IsNullOrEmpty($SubCategory) -and [string]::IsNullOrEmpty($Description)) {
            throw "Assignee, GroupAssignee, GroupAssigneeId, Status, Category, SubCategory, or Description must be provided."
        }
        Write-Verbose "Requesting Incident $Number"
        $incident = Get-SwSdIncident -Number $Number
        if (!$incident) {
            throw "Incident $Number not found."
        }
        $msg = ""
        $body = @{ incident = @{} }

        if (![string]::IsNullOrEmpty($Status)) {
            $msg += "Status: $($Status.Trim())"
            $body.incident.state = "$($Status.Trim())"
        }
        $targetQueueId = $null
        $targetQueueName = $null
        $primaryRejectedQueueIdPayload = $false
        $lastSuccessfulQueuePayload = $null
        if ($GroupAssigneeId -and [string]::IsNullOrWhiteSpace($GroupAssignee)) {
            try {
                $queueById = Get-SwSdQueue -Id $GroupAssigneeId | Select-Object -First 1
                if ($queueById -and $queueById.name) {
                    $targetQueueName = [string]$queueById.name
                    Write-Verbose "Resolved queue name for id $($GroupAssigneeId): $($targetQueueName)"
                }
            } catch {
            }
        }

        if (![string]::IsNullOrEmpty($Assignee)) {
            Write-Verbose "Verifying User $Assignee"
            $user = Get-SwSdUser -Email $Assignee
            if (!$user) {
                throw "User $Assignee not found."
            }
            $msg += "Assignee: $($Assignee.Trim())"
            $body.incident.assignee = @{
                email = "$($Assignee.Trim())"
            }
        } elseif ($GroupAssigneeId) {
            $msg += "Group Assignee Id: $GroupAssigneeId"
            $targetQueueId = [int]$GroupAssigneeId
            $body.incident.group_assignee_id = $targetQueueId
            $body.incident.assignee = $null
        } elseif (![string]::IsNullOrEmpty($GroupAssignee)) {
            $targetGroupName = $GroupAssignee.Trim()
            $queue = $null
            $group = $null
            Write-Verbose "Verifying Queue/Group $targetGroupName"
            try {
                $queue = Get-SwSdQueue -Name $targetGroupName | Select-Object -First 1
            } catch {
            }
            if (!$queue -or !$queue.id) {
                $group = Get-SwSdGroup -Name $targetGroupName | Select-Object -First 1
            }
            if ($queue -and $queue.id) {
                $msg += "Queue Assignee Id: $($queue.id)"
                $targetQueueId = [int]$queue.id
                $body.incident.group_assignee_id = $targetQueueId
                $body.incident.assignee = $null
            } elseif ($group -and $group.id) {
                $msg += "Group Assignee Id: $($group.id)"
                $targetQueueId = [int]$group.id
                $body.incident.group_assignee_id = $targetQueueId
                $body.incident.assignee = $null
            } else {
                $msg += "Group Assignee: $targetGroupName"
                $targetQueueName = $targetGroupName
                $body.incident.group_assignee = @{ name = $targetQueueName }
            }
        }
        if (![string]::IsNullOrEmpty($Category)) {
            Write-Verbose "Verifying Category $Category"
            $category = Get-SwSdCatalogCategory -Name $Category
            if (!$category) {
                throw "Category $Category not found."
            }
            $msg += "Category: $($Category.Trim())"
            $body.incident.category = @{
                name = "$($Category.Trim())"
            }
        }
        if (![string]::IsNullOrEmpty($SubCategory)) {
            Write-Verbose "Verifying SubCategory $SubCategory"
            $subcategory = Get-SwSdCatalogSubCategory -Name $SubCategory
            if (!$subcategory) {
                throw "SubCategory $SubCategory not found."
            }
            $msg += "SubCategory: $($SubCategory.Trim())"
            $body.incident.subcategory = @{
                name = "$($SubCategory.Trim())"
            }
        }
        if (![string]::IsNullOrEmpty($Description)) {
            $msg += "Description"
            $body.incident.description = "$($Description.Trim())"
        }

        if ($body.incident.Keys.Count -eq 0) {
            throw "No valid update fields were constructed for incident $Number."
        }
        $url = $incident.href
        Write-Verbose "Updating incident at URL: $($url)"
        $requestResult = Invoke-IncidentUpdateRequest -Uri $url -Headers $SDSession.headers -IncidentBody $body.incident
        if ($requestResult.Success) {
            Write-Verbose "Incident $Number update HTTP status: $($requestResult.StatusCode)"
        } else {
            Write-Verbose "Primary update failed. HTTP status: $($requestResult.StatusCode)"
            if ($requestResult.ErrorBody) {
                Write-Verbose "Primary update error body: $($requestResult.ErrorBody)"
                if ($requestResult.ErrorBody -match 'group_assignee') {
                    $primaryRejectedQueueIdPayload = $true
                }
            }
        }

        if (($targetQueueId -or $targetQueueName) -and -not $requestResult.Success) {
            Write-Verbose "Retrying queue assignment with fallback payloads."
            $fallbackPayloads = @()
            if ($targetQueueId) {
                $fallbackPayloads += @{ assignee = $null; group_assignee = @{ id = $targetQueueId } }
                $fallbackPayloads += @{ assignee = @{ id = $targetQueueId } }
                $fallbackPayloads += @{ assignee = @{ id = $targetQueueId }; group_assignee = @{ id = $targetQueueId } }
                $fallbackPayloads += @{ assignee = $null; group_assignee = @{ name = [string]$targetQueueId } }
                $fallbackPayloads += @{ assignee = @{ name = [string]$targetQueueId } }
                if ($targetQueueName) {
                    $fallbackPayloads += @{ assignee = $null; group_assignee = @{ name = $targetQueueName } }
                    $fallbackPayloads += @{ assignee = @{ name = $targetQueueName } }
                    $fallbackPayloads += @{ assignee = @{ name = $targetQueueName }; group_assignee = @{ name = $targetQueueName } }
                }
                if ($GroupAssignee) {
                    $fallbackPayloads += @{ assignee = $null; group_assignee = @{ name = $GroupAssignee.Trim() } }
                    $fallbackPayloads += @{ assignee = @{ name = $GroupAssignee.Trim() } }
                }
            } elseif ($targetQueueName) {
                $fallbackPayloads += @{ assignee = $null; group_assignee = @{ name = $targetQueueName } }
                $fallbackPayloads += @{ assignee = @{ name = $targetQueueName } }
            }

            foreach ($fallbackIncident in $fallbackPayloads) {
                $requestResult = Invoke-IncidentUpdateRequest -Uri $url -Headers $SDSession.headers -IncidentBody $fallbackIncident
                if ($requestResult.Success) {
                    Write-Verbose "Fallback update succeeded. HTTP status: $($requestResult.StatusCode)"
                    $lastSuccessfulQueuePayload = $fallbackIncident
                    break
                }
                Write-Verbose "Fallback update failed. HTTP status: $($requestResult.StatusCode)"
                if ($requestResult.ErrorBody) {
                    Write-Verbose "Fallback update error body: $($requestResult.ErrorBody)"
                }
            }
        }

        if (-not $requestResult.Success) {
            $finalMessage = "Incident update failed."
            if ($requestResult.StatusCode) {
                $finalMessage += " HTTP status: $($requestResult.StatusCode)."
            }
            if ($requestResult.ErrorBody) {
                $finalMessage += " Response: $($requestResult.ErrorBody)"
            }
            throw $finalMessage
        }

        if ($targetQueueId -or $targetQueueName) {
            $updatedIncident = Get-IncidentByHref -Uri $url -Headers $SDSession.headers
            $match = Test-QueueAssignmentMatch -IncidentObject $updatedIncident -QueueId $targetQueueId -QueueName $targetQueueName
            $queueUpdated = $match.Matched

            if (-not $queueUpdated) {
                Write-Verbose "Queue assignment not confirmed after first payload. Retrying with fallback assignment payload."
                $retryIncident = @{}
                if ($targetQueueId) {
                    if ($lastSuccessfulQueuePayload) {
                        $retryIncident = $lastSuccessfulQueuePayload
                    } elseif ($primaryRejectedQueueIdPayload) {
                        $retryIncident.assignee = $null
                        $retryIncident.group_assignee = @{ id = $targetQueueId }
                    } else {
                        $retryIncident.assignee = $null
                        $retryIncident.group_assignee_id = $targetQueueId
                    }
                } else {
                    $retryIncident.assignee = $null
                    $retryIncident.group_assignee = @{ name = $targetQueueName }
                }
                $requestResult = Invoke-IncidentUpdateRequest -Uri $url -Headers $SDSession.headers -IncidentBody $retryIncident
                if ($requestResult.Success) {
                    Write-Verbose "Retry update HTTP status: $($requestResult.StatusCode)"
                } else {
                    Write-Verbose "Retry update failed. HTTP status: $($requestResult.StatusCode)"
                    if ($requestResult.ErrorBody) {
                        Write-Verbose "Retry update error body: $($requestResult.ErrorBody)"
                    }
                }

                $updatedIncident = Get-IncidentByHref -Uri $url -Headers $SDSession.headers
                $match = Test-QueueAssignmentMatch -IncidentObject $updatedIncident -QueueId $targetQueueId -QueueName $targetQueueName
                $queueUpdated = $match.Matched
                Write-Verbose "Post-retry assignment check: group_assignee.id='$($match.GroupId)', group_assignee.name='$($match.GroupName)', assignee.id='$($match.AssigneeId)', assignee.name='$($match.AssigneeName)', assignee.is_user='$($match.AssigneeUser)', matched=$queueUpdated"
            }

            if (-not $queueUpdated) {
                throw "Queue assignment was not confirmed on incident $Number after update attempts."
            }
        }
        $result = [pscustomobject]@{
            Status        = "Success"
            State         = $Status
            Assignee      = $Assignee
            GroupAssignee = $GroupAssignee
            Category      = $Category
            SubCategory   = $SubCategory
            Description   = $Description
        }
        Write-Verbose "Update details: $($msg -join ("; "))"
    } catch {
        $result = [pscustomobject]@{
            Status   = "Error"
            Activity = $($_.CategoryInfo.Activity -join (";"))
            Message  = $($_.Exception.Message -join (";"))
            Trace    = $($_.ScriptStackTrace -join (";"))
        }
    } finally {
        $result
    }
}