Src/Private/Get-AbrIntuneFailedAssignments.ps1

function Get-AbrIntuneFailedAssignments {
    <#
    .SYNOPSIS
    Documents Intune policies with failed device-level deployment status.
    .DESCRIPTION
        Collects and reports on:
          - Compliance policies with devices in Error or Failed state
          - Configuration profiles with Error deployment state
          - Summary table of policy name, platform, failed count, error count
    .NOTES
        Version: 0.1.0
        Author: Pai Wei Sing
    #>

    [CmdletBinding()]
    param (
        [Parameter(Position = 0, Mandatory)]
        [string]$TenantId
    )

    begin {
        Write-PScriboMessage -Message "Collecting Intune Failed Assignments for $TenantId."
        Show-AbrDebugExecutionTime -Start -TitleMessage 'Failed Assignments'
    }

    process {
        Section -Style Heading2 'Failed Assignments' {
            Paragraph "The following section documents Intune policies with deployment failures at the device level for tenant $TenantId."
            BlankLine

            $failedObj = [System.Collections.ArrayList]::new()
            $foundAny  = $false

            #region Compliance Policy Device Statuses
            try {
                Write-Host " - Retrieving compliance policy device statuses..."
                $CompPoliciesResp = Invoke-MgGraphRequest -Method GET `
                    -Uri "$($script:GraphEndpoint)/v1.0/deviceManagement/deviceCompliancePolicies?`$select=id,displayName" `
                    -ErrorAction SilentlyContinue
                $CompPolicies = $CompPoliciesResp.value

                if ($CompPolicies) {
                    foreach ($Policy in $CompPolicies) {
                        try {
                            $StatusResp = Invoke-MgGraphRequest -Method GET `
                                -Uri "$($script:GraphEndpoint)/v1.0/deviceManagement/deviceCompliancePolicies/$($Policy.id)/deviceStatusOverview" `
                                -ErrorAction SilentlyContinue
                            if ($StatusResp -and ($StatusResp.errorCount -gt 0 -or $StatusResp.failedCount -gt 0)) {
                                $foundAny = $true
                                $failInObj = [ordered] @{
                                    'Policy Name'    = $Policy.displayName
                                    'Policy Type'    = 'Compliance Policy'
                                    'Failed Devices' = $StatusResp.failedCount
                                    'Error Devices'  = $StatusResp.errorCount
                                    'Success Devices'= $StatusResp.succeededCount
                                    'Pending Devices'= $StatusResp.pendingCount
                                    'Not Applicable' = $StatusResp.notApplicableCount
                                }
                                $null = $failedObj.Add([pscustomobject]$failInObj)
                            }
                        } catch { }
                    }
                }
            } catch {
                if (Test-AbrGraphForbidden -ErrorRecord $_) {
                    Write-AbrPermissionError -Section 'Compliance Policy Statuses' -RequiredRole 'Intune Service Administrator or Global Administrator'
                } else {
                    Write-AbrSectionError -Section 'Compliance Policy Statuses' -Message "$($_.Exception.Message)"
                }
            }
            #endregion

            #region Configuration Profile Device Statuses
            try {
                Write-Host " - Retrieving configuration profile device statuses..."
                $ConfigPoliciesResp = Invoke-MgGraphRequest -Method GET `
                    -Uri "$($script:GraphEndpoint)/v1.0/deviceManagement/deviceConfigurations?`$select=id,displayName" `
                    -ErrorAction SilentlyContinue
                $ConfigPolicies = $ConfigPoliciesResp.value

                if ($ConfigPolicies) {
                    foreach ($Policy in $ConfigPolicies) {
                        try {
                            $StatusResp = Invoke-MgGraphRequest -Method GET `
                                -Uri "$($script:GraphEndpoint)/v1.0/deviceManagement/deviceConfigurations/$($Policy.id)/deviceStatusOverview" `
                                -ErrorAction SilentlyContinue
                            if ($StatusResp -and ($StatusResp.errorCount -gt 0 -or $StatusResp.failedCount -gt 0)) {
                                $foundAny = $true
                                $failInObj = [ordered] @{
                                    'Policy Name'    = $Policy.displayName
                                    'Policy Type'    = 'Configuration Profile'
                                    'Failed Devices' = $StatusResp.failedCount
                                    'Error Devices'  = $StatusResp.errorCount
                                    'Success Devices'= $StatusResp.succeededCount
                                    'Pending Devices'= $StatusResp.pendingCount
                                    'Not Applicable' = $StatusResp.notApplicableCount
                                }
                                $null = $failedObj.Add([pscustomobject]$failInObj)
                            }
                        } catch { }
                    }
                }
            } catch {
                if (Test-AbrGraphForbidden -ErrorRecord $_) {
                    Write-AbrPermissionError -Section 'Configuration Profile Statuses' -RequiredRole 'Intune Service Administrator or Global Administrator'
                } else {
                    Write-AbrSectionError -Section 'Configuration Profile Statuses' -Message "$($_.Exception.Message)"
                }
            }
            #endregion

            if ($foundAny -and $failedObj.Count -gt 0) {
                # Sort by most failed first
                $failedObj = [System.Collections.ArrayList]($failedObj | Sort-Object { [int]$_.'Failed Devices' + [int]$_.'Error Devices' } -Descending)

                $null = (& {
                    if ($HealthCheck.Intune.FailedAssignments) {
                        $null = ($failedObj | Where-Object { [int]$_.'Failed Devices' -gt 0 } | Set-Style -Style Critical | Out-Null)
                        $null = ($failedObj | Where-Object { [int]$_.'Error Devices' -gt 0 -and [int]$_.'Failed Devices' -eq 0 } | Set-Style -Style Warning | Out-Null)
                    }
                })

                $FailTableParams = @{ Name = "Failed Policy Deployments - $TenantId"; ColumnWidths = 28, 20, 11, 11, 11, 11, 8 }
                if ($Report.ShowTableCaptions) { $FailTableParams['Caption'] = "- $($FailTableParams.Name)" }
                $failedObj | Table @FailTableParams

                if (Get-IntuneExcelSheetEnabled -SheetKey 'FailedAssignments') {
                    $script:ExcelSheets['Failed Assignments'] = $failedObj
                }

                BlankLine
                Paragraph "Total policies with deployment failures: $($failedObj.Count). Review affected devices in the Intune portal under Devices > Monitor."
            } else {
                Paragraph "No policy deployment failures detected in tenant $TenantId."
            }
        }
    }

    end {
        Show-AbrDebugExecutionTime -End -TitleMessage 'Failed Assignments'
    }
}