Public/Get-GkGroupBasedLicense.ps1

function Get-GkGroupBasedLicense {
    <#
    .SYNOPSIS
        Report groups that assign licenses, and whether their assignment has finished processing.

    .DESCRIPTION
        Reads GET /groups selecting assignedLicenses and licenseProcessingState, and returns only
        the groups that actually carry a license assignment. Group-based licensing is invisible in
        most reports until it breaks, and licenseProcessingState is the field that tells you a
        change is still rolling out — or stuck.

        Pairs with Get-GkLicenseAssignmentError, which reports the affected *users*; this reports
        the group that assigned the licence to them. Requires Group.Read.All (or Directory.Read.All).

    .PARAMETER Name
        Only return groups whose display name contains this text (case-insensitive).

    .PARAMETER NotProcessedOnly
        Only return groups whose licenseProcessingState is not ProcessingComplete — the ones still
        rolling out, or stuck.

    .PARAMETER ResolveSkuName
        Resolve each skuId GUID to its SKU part number and friendly name. Costs one extra call to
        /subscribedSkus and therefore needs a second scope (Organization.Read.All or
        Directory.Read.All), which is only validated when you ask for it.

    .PARAMETER First
        Return at most this many groups.

    .PARAMETER AsReport
        Add a ReportGeneratedUtc column and flatten the SKU list to a string.

    .EXAMPLE
        Get-GkGroupBasedLicense

        Every group that assigns a licence.

    .EXAMPLE
        Get-GkGroupBasedLicense -NotProcessedOnly

        Group licence assignments that have not finished applying.

    .EXAMPLE
        Get-GkGroupBasedLicense -AsReport | Export-Csv .\group-licensing.csv -NoTypeInformation

    .OUTPUTS
        PSGraphKit.GroupBasedLicense
    #>

    [CmdletBinding()]
    [OutputType('PSGraphKit.GroupBasedLicense')]
    param(
        [string] $Name,

        [switch] $NotProcessedOnly,

        [switch] $ResolveSkuName,

        [int] $First,

        [switch] $AsReport
    )

    begin {
        # Resolving SKU names reads /subscribedSkus, which needs a scope the base cmdlet does not.
        $scopeVariant = ''
        if ($ResolveSkuName) { $scopeVariant = 'ResolveSkuName' }
        Test-GkConnection -FunctionName 'Get-GkGroupBasedLicense' -Variant $scopeVariant -Caller $PSCmdlet | Out-Null
        $now = [datetime]::UtcNow

        $skuNames = @{}
        if ($ResolveSkuName) {
            try {
                foreach ($s in (Invoke-GkGraphRequest -Uri '/subscribedSkus?$select=skuId,skuPartNumber' -CallerFunction 'Get-GkGroupBasedLicense')) {
                    $skuNames[[string](Get-GkDictValue $s 'skuId')] = [string](Get-GkDictValue $s 'skuPartNumber')
                }
            }
            catch {
                Write-Warning "Could not read /subscribedSkus; reporting skuId GUIDs instead. $($_.Exception.Message)"
            }
        }
    }

    process {
        # assignedLicenses is not filterable server-side, so pull the selected fields and drop the
        # groups with no assignment client-side. The $select keeps the payload small.
        $params = @{
            Uri            = '/groups?$select=id,displayName,assignedLicenses,licenseProcessingState'
            CallerFunction = 'Get-GkGroupBasedLicense'
        }
        if ($PSBoundParameters.ContainsKey('First')) { $params['MaxResult'] = $First }

        $groups = Invoke-GkGraphRequest @params

        foreach ($g in $groups) {
            $assigned = @(Get-GkDictValue $g 'assignedLicenses')
            if ($assigned.Count -eq 0) { continue }

            $display = [string](Get-GkDictValue $g 'displayName')
            if ($Name -and $display -notlike "*$Name*") { continue }

            # licenseProcessingState is an object with a single 'state' property.
            $stateObj = Get-GkDictValue $g 'licenseProcessingState'
            $state = [string](Get-GkDictValue $stateObj 'state')
            if ($NotProcessedOnly -and $state -eq 'ProcessingComplete') { continue }

            $skus = @(
                foreach ($a in $assigned) {
                    $id = [string](Get-GkDictValue $a 'skuId')
                    $part = $skuNames[$id]
                    if (-not $part) { $id }
                    elseif ($script:GkSkuFriendlyName.ContainsKey($part)) { $script:GkSkuFriendlyName[$part] }
                    else { $part }
                }
            )

            $obj = [ordered]@{
                PSTypeName      = 'PSGraphKit.GroupBasedLicense'
                DisplayName     = $display
                LicenseCount    = $assigned.Count
                ProcessingState = if ($state) { $state } else { 'Unknown' }
                IsComplete      = ($state -eq 'ProcessingComplete')
                Id              = [string](Get-GkDictValue $g 'id')
            }
            if ($AsReport) { $obj['Skus'] = ($skus -join '; ') }
            else { $obj['Skus'] = [string[]]@($skus) }
            if ($AsReport) { $obj['ReportGeneratedUtc'] = $now }
            [pscustomobject]$obj
        }
    }
}