Providers/Okta/Private/Get-OktaUserHeadroom.ps1

function Get-OktaUserHeadroom {
    <#
    .SYNOPSIS
        Reports how many more users the tenant will accept before it hits its licence ceiling

    .DESCRIPTION
        The Okta Integrator Free Plan allows ten active users, and your own admin account is
        one of them. That is the single constraint this whole module is shaped around, and the
        reason it seeds eight users rather than the hundreds a test-data generator would
        normally produce.

        The count comes from GET /api/v1/users, which by default lists everything that is not
        DEPROVISIONED. That is the same population the licence counts, so a deactivated user
        left behind from a previous run does not eat headroom, but a suspended or staged one
        does. Both of those were confirmed against a real tenant rather than assumed.

        The important distinction is between Available and AvailableForSeed. A user this module
        already created needs no new licence slot, because seeding updates it rather than
        creating a second one. Counting those against the ceiling would mean the module refuses
        to re-run against the environment it just built, which is exactly the situation you are
        in after a seed run fails halfway and you want to fix the cause and try again.

    .PARAMETER ActiveUserLimit
        The tenant's active user ceiling. Ten for the Integrator Free Plan.

    .OUTPUTS
        PSCustomObject with Limit, InUse, Available, SeededInUse, AvailableForSeed and
        ExistingLogins

    .EXAMPLE
        $headroom = Get-OktaUserHeadroom -ActiveUserLimit 10
        if ($headroom.AvailableForSeed -lt 8) { throw 'Not enough room' }

    .NOTES
        Author: Jeffrey Stuhr
        Version: 1.1.0
        Last Updated: 2026-08-07
    #>


    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param(
        [Parameter()]
        [ValidateRange(1, 10000)]
        [int]$ActiveUserLimit = 10
    )

    $connection = Get-OktaConnection

    $existing = @(Invoke-OktaRequest -Method GET -Path '/api/v1/users' -Query @{ limit = 200 } -Paginate)

    # Classified from the listing already in hand rather than by calling Get-OktaSeededUser,
    # which would repeat the same request. The rule matches that function's: the seed tag, or
    # the seed email domain as a true suffix.
    $suffix = '@' + $connection.EmailDomain.TrimStart('@')
    $seeded = @($existing | Where-Object {
        $userProfile = $_.profile
        ($userProfile.PSObject.Properties['labSeedTag'] -and
            $userProfile.labSeedTag -eq (Get-OktaSeedTag -Prefix $connection.Prefix)) -or
        ($userProfile.login -and
            $userProfile.login.EndsWith($suffix, [StringComparison]::OrdinalIgnoreCase))
    })

    $inUse = $existing.Count
    $available = [Math]::Max(0, $ActiveUserLimit - $inUse)

    return [PSCustomObject]@{
        Limit            = $ActiveUserLimit
        InUse            = $inUse
        Available        = $available
        SeededInUse      = $seeded.Count
        AvailableForSeed = $available + $seeded.Count
        ExistingLogins   = @($existing | ForEach-Object { $_.profile.login })
    }
}