Private/Invoke-AACCostQuery.ps1

function Invoke-AACCostQuery {
    <#
    .SYNOPSIS
        Runs a Cost Management query for one subscription and returns its
        rows as objects with one property per column.
    .DESCRIPTION
        POST https://management.azure.com/subscriptions/<id>/providers/
        Microsoft.CostManagement/query (api-version 2023-11-01), following
        nextLink paging. The response's column/row arrays become objects, so
        a query grouped by ServiceName returns rows like
        @{ Cost = 12.3; ServiceName = 'Storage'; Currency = 'USD' }.
 
        Cost Management allows only a few queries per minute per scope, so a
        throttled (429) or transient (5xx) response is retried up to five
        times, waiting as long as the response's
        x-ms-ratelimit-microsoft.costmanagement-*-retry-after or Retry-After
        header asks (5-60 seconds otherwise). Any other error is thrown with
        Azure's own message.
    #>

    [CmdletBinding()]
    [OutputType([pscustomobject])]
    param(
        [Parameter(Mandatory)]
        [string] $SubscriptionId,

        [Parameter(Mandatory)]
        [hashtable] $Body
    )

    # No web-request progress bar over the caller's Spectre display.
    $ProgressPreference = 'SilentlyContinue'
    $uri = "https://management.azure.com/subscriptions/$SubscriptionId/providers/Microsoft.CostManagement/query?api-version=2023-11-01"
    $json = $Body | ConvertTo-Json -Depth 10

    while ($uri) {
        # Through the pooled HttpClient: Invoke-AACHttp retries throttling as
        # long as Cost Management's retry-after headers ask, and gives its
        # own error message. Months arrive as [datetime], as they did from
        # Invoke-RestMethod.
        try {
            $content = (Invoke-AACHttp -Method Post -Uri $uri -Body $json).Content
        }
        catch {
            throw $_.Exception.Message
        }
        $response = ConvertFrom-Json -InputObject $content -Depth 100

        $properties = Get-AACPropertyValue -InputObject $response -Name 'properties'
        $columns = @(Get-AACPropertyValue -InputObject $properties -Name 'columns' | ForEach-Object { $_.name })
        foreach ($row in @(Get-AACPropertyValue -InputObject $properties -Name 'rows')) {
            $item = [ordered]@{}
            for ($i = 0; $i -lt $columns.Count; $i++) {
                $item[$columns[$i]] = $row[$i]
            }
            [pscustomobject]$item
        }
        $uri = Get-AACPropertyValue -InputObject $properties -Name 'nextLink'
    }
}