Public/Get-PCOciUsage.ps1

function Get-PCOciUsage {
    <#
    .SYNOPSIS
        Queries the Usage API for a daily series of cost or usage and writes it to a file.

    .DESCRIPTION
        Daily granularity, cost, and no aggregation over time unless asked (decision D-009), so the file holds one
        row per day per group and the month-on-month question can be answered from it. A window longer than 90
        days is split into several queries; a window ending within 48 hours of now is flagged in the report
        because the service still receives late usage for it. Up to four group-by dimensions.

    .PARAMETER Start
        The first day, inclusive. Default: the first day of the previous calendar month, UTC.

    .PARAMETER End
        The day after the last day, exclusive. Default: the first day of the current calendar month, UTC.

    .PARAMETER GroupBy
        Up to four of: service, skuName, skuPartNumber, unit, compartmentName, compartmentPath, compartmentId,
        platform, region, logicalAd, resourceId, resourceName, tenantId, tenantName.

    .PARAMETER AggregateByTime
        One row per group for the whole window instead of one per period. The file then says granularity TOTAL.

    .EXAMPLE
        Get-PCOciUsage -GroupBy service, compartmentPath -Path .\data\usage-2026-10.json -Deterministic
    #>

    [CmdletBinding(SupportsShouldProcess)]
    [OutputType([pscustomobject], [PowerCraft.OCI.Core.Usage.UsageSeries])]
    param(
        [PowerCraft.OCI.Core.Identity.OciContext]$Context,
        [datetime]$Start,
        [datetime]$End,
        [ValidateSet('Cost', 'Usage')][string]$QueryType = 'Cost',
        [ValidateSet('Daily', 'Hourly', 'Monthly')][string]$Granularity = 'Daily',
        [ValidateCount(0, 4)]
        [ValidateSet('service', 'skuName', 'skuPartNumber', 'unit', 'compartmentName', 'compartmentPath', 'compartmentId', 'platform', 'region', 'logicalAd', 'resourceId', 'resourceName', 'tenantId', 'tenantName')]
        [string[]]$GroupBy,
        [switch]$AggregateByTime,
        [ValidateRange(0, 6)][int]$CompartmentDepth = 6,
        [string]$Path = (Join-Path (Get-Location).Path 'usage.json'),
        [switch]$Deterministic,
        [string]$ReportPath,
        [switch]$PassThru,
        [switch]$Force
    )

    $ctx = Get-PCOciContextOrThrow -Context $Context
    if ((Test-Path -LiteralPath $Path) -and -not $Force) { throw "A file already exists at '$Path'. Use -Force to overwrite it." }
    if (-not $ReportPath) { $ReportPath = [IO.Path]::ChangeExtension($Path, '.report.json') }

    $today = [DateTime]::UtcNow.Date
    $thisMonth = [DateTime]::new($today.Year, $today.Month, 1, 0, 0, 0, [DateTimeKind]::Utc)
    if (-not $PSBoundParameters.ContainsKey('End')) { $End = $thisMonth }
    if (-not $PSBoundParameters.ContainsKey('Start')) { $Start = $thisMonth.AddMonths(-1) }

    $query = [PowerCraft.OCI.Core.Usage.UsageQuery]::new()
    $query.TenancyId = $ctx.TenancyId
    $query.Start = [DateTimeOffset]::new([DateTime]::SpecifyKind($Start, [DateTimeKind]::Utc))
    $query.End = [DateTimeOffset]::new([DateTime]::SpecifyKind($End, [DateTimeKind]::Utc))
    $query.QueryType = $QueryType.ToUpperInvariant()
    $query.Granularity = $Granularity.ToUpperInvariant()
    if ($GroupBy) { $query.GroupBy.AddRange([string[]]$GroupBy) }
    $query.AggregateByTime = [bool]$AggregateByTime
    $query.CompartmentDepth = $CompartmentDepth

    $plan = [PowerCraft.OCI.Core.Usage.UsagePlanner]::Plan($query, [DateTimeOffset]::UtcNow)
    if (-not $PSCmdlet.ShouldProcess($Path, "Query $($query.QueryType) $($query.Granularity) for $($plan.Windows.Count) window(s)")) { return }

    $report = [PowerCraft.OCI.Core.Report.RunReport]::new()
    $report.Verb = 'Get-PCOciUsage'
    $started = [DateTimeOffset]::UtcNow
    $report.StartedAt = $started
    $series = [PowerCraft.OCI.Core.Gateways.UsageGateway]::Run($ctx, $query, $plan, $report)
    $duration = [long]([DateTimeOffset]::UtcNow - $started).TotalMilliseconds
    $report.DurationMs = $duration

    [PowerCraft.OCI.Core.Usage.UsageWriter]::Write($series, $Path, [bool]$Deterministic)
    $report.Write($ReportPath, [bool]$Deterministic)
    foreach ($w in $report.Warnings) { Write-Warning $w }
    if (-not $series.Complete) { Write-Warning ("The usage series is incomplete: {0} failure(s). See '{1}'." -f $report.Failures.Count, $ReportPath) }

    if ($PassThru) { return $series }
    [pscustomobject]@{
        UsagePath   = (Resolve-Path -LiteralPath $Path).Path
        ReportPath  = (Resolve-Path -LiteralPath $ReportPath).Path
        Complete    = $series.Complete
        Start       = $series.Start
        End         = $series.End
        QueryType   = $series.QueryType
        Granularity = $series.Granularity
        GroupBy     = @($series.GroupBy)
        Windows     = $plan.Windows.Count
        Rows        = $series.Rows.Count
        Currency    = $series.Currency
        DurationMs  = $duration
    }
}