Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion docs-mslearn/toolkit/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ title: FinOps toolkit changelog
description: Review the latest features and enhancements in the FinOps toolkit, including updates to FinOps hubs, Power BI reports, and more.
author: MSBrett
ms.author: brettwil
ms.date: 08/13/2026
ms.date: 08/17/2026
ms.topic: reference
ms.service: finops
ms.subservice: finops-toolkit
Expand Down Expand Up @@ -59,6 +59,11 @@ The following section lists features and enhancements that are currently in deve
- **Changed**
- Switched the reservations and benefits workbooks from the retired `ccmstorageprod` isfratioblob.csv to the FinOps toolkit [Instance size flexibility](open-data.md#instance-size-flexibility) open data file ([#2090](https://github.com/microsoft/finops-toolkit/issues/2090)).

### [PowerShell module](powershell/powershell-commands.md)

- **Fixed**
- Fixed [Start-FinOpsCostExport](powershell/cost/start-finopscostexport.md) exporting the wrong period for anyone running in a positive UTC offset. `-StartDate` and `-EndDate` are now treated as UTC calendar dates instead of being time zone converted, so the days you request are the days that get exported. Previously, local midnight converted to the previous UTC day, which moved the period back a day and made `-Backfill` run one extra month ([#2255](https://github.com/microsoft/finops-toolkit/issues/2255)).

### [Open data](open-data.md) updates

**[Instance size flexibility](open-data.md#instance-size-flexibility)**
Expand Down
18 changes: 9 additions & 9 deletions docs-mslearn/toolkit/powershell/cost/start-finopscostexport.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ title: Start-FinOpsCostExport command
description: Initiate a Cost Management export run for the most recent period using the Start-FinOpsCostExport command in the FinOpsToolkit module.
author: flanakin
ms.author: micflan
ms.date: 04/01/2026
ms.date: 08/17/2026
ms.topic: reference
ms.service: finops
ms.subservice: finops-toolkit
Expand Down Expand Up @@ -39,14 +39,14 @@ Start-FinOpsCostExport `

## Parameters

| Name | Description |
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `‑Name` | Required. Name of the export. |
| `‑Scope` | Optional. Resource ID of the scope to export data for. If empty, defaults to current subscription context. |
| `‑StartDate` | Optional. Day to start pulling the data for. If not set, the export uses the dates defined in the export configuration. |
| `‑EndDate` | Optional. Last day to pull data for. If not set and -StartDate is set, -EndDate uses the last day of the month. If not set and -StartDate isn't set, the export uses the dates defined in the export configuration. |
| `‑Backfill` | Optional. Number of months to export the data for. Make note of throttling (429) errors. It only runs once. Failed exports aren't reattempted. Default = 0. |
| `‑ApiVersion` | Optional. API version to use when calling the Cost Management Exports API. Default = 2025-03-01. |
| Name | Description |
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `‑Name` | Required. Name of the export. |
| `‑Scope` | Optional. Resource ID of the scope to export data for. If empty, defaults to current subscription context. |
| `‑StartDate` | Optional. Day to start pulling the data for. Interpreted as a UTC calendar date, so the day you specify is the day that gets exported, regardless of your local time zone. If not set, the export uses the dates defined in the export configuration. |
| `‑EndDate` | Optional. Last day to pull data for. Interpreted as a UTC calendar date, so the day you specify is the day that gets exported, regardless of your local time zone. If not set and -StartDate is set, -EndDate uses the last day of the month. If not set and -StartDate isn't set, the export uses the dates defined in the export configuration. |
| `‑Backfill` | Optional. Number of months to export the data for. Make note of throttling (429) errors. It only runs once. Failed exports aren't reattempted. Default = 0. |
| `‑ApiVersion` | Optional. API version to use when calling the Cost Management Exports API. Default = 2025-03-01. |

<br>

Expand Down
31 changes: 22 additions & 9 deletions src/powershell/Public/Start-FinOpsCostExport.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@
Optional. Resource ID of the scope to export data for. If empty, defaults to current subscription context.

.PARAMETER StartDate
Optional. Day to start pulling the data for. If not set, the export will use the dates defined in the export configuration.
Optional. Day to start pulling the data for. Interpreted as a UTC calendar date, so the day you specify is the day that is exported, regardless of the local time zone. If not set, the export will use the dates defined in the export configuration.

.PARAMETER EndDate
Optional. Last day to pull data for. If not set and -StartDate is set, -EndDate will use the last day of the month. If not set and -StartDate is not set, the export will use the dates defined in the export configuration.
Optional. Last day to pull data for. Interpreted as a UTC calendar date, so the day you specify is the day that is exported, regardless of the local time zone. If not set and -StartDate is set, -EndDate will use the last day of the month. If not set and -StartDate is not set, the export will use the dates defined in the export configuration.

.PARAMETER Backfill
Optional. Number of months to export the data for. Make note of throttling (429) errors. This is only run once. Failed exports are not re-attempted. Default = 0.
Expand Down Expand Up @@ -91,6 +91,19 @@ function Start-FinOpsCostExport

$runpath = "$($export.Id)/run?api-version=$ApiVersion"

# -StartDate and -EndDate are calendar dates, not instants. Cost Management export periods
# are UTC and day-granular, so keep the day the caller named and tag it as UTC. Converting
# with ToUniversalTime() would move the period back a day for every caller east of UTC,
# where local midnight falls on the previous UTC day.
if ($StartDate)
{
$StartDate = [datetime]::SpecifyKind($StartDate.Date, [DateTimeKind]::Utc)
}
if ($EndDate)
{
$EndDate = [datetime]::SpecifyKind($EndDate.Date, [DateTimeKind]::Utc)
}

# Set start date if using -Backfill
if ($Backfill -gt 0)
{
Expand All @@ -99,33 +112,33 @@ function Start-FinOpsCostExport
# If -StartDate is not set, assume the current month
if (-not $StartDate)
{
$StartDate = (Get-Date -Day 1 -Hour 0 -Minute 0 -Second 0 -Millisecond 0).ToUniversalTime().Date
$utcToday = (Get-Date).ToUniversalTime().Date
$StartDate = $utcToday.AddDays(1 - $utcToday.Day)
}

# If -EndDate is not set, assume 1 month
if (-not $EndDate)
{
$EndDate = $StartDate.AddMonths(1).AddMilliseconds(-1)
$EndDate = $StartDate.AddMonths(1).AddDays(-1)
}

# Move start date to account for the backfill period
$StartDate = $StartDate.AddMonths($Backfill * -1)
Write-Verbose "Backfill $Backfill months = $($StartDate.ToUniversalTime().ToString('yyyy-MM-dd"T"HH:mm:ss"Z"')) to $($EndDate.ToUniversalTime().ToString('yyyy-MM-dd"T"HH:mm:ss"Z"'))"
Write-Verbose "Backfill $Backfill months = $($StartDate.ToString('yyyy-MM-dd"T"HH:mm:ss"Z"')) to $($EndDate.ToString('yyyy-MM-dd"T"HH:mm:ss"Z"'))"
}

# Remove time + set end date
if ($StartDate)
{
$StartDate = $StartDate.ToUniversalTime().Date
if ($EndDate)
{
$EndDate = $EndDate.ToUniversalTime().Date
$EndDate = $EndDate.Date
}
else
{
$EndDate = $StartDate.ToUniversalTime().Date.AddMonths(1).AddDays(-1)
$EndDate = $StartDate.AddMonths(1).AddDays(-1)
}
Write-Verbose "Updated dates = $($StartDate.ToUniversalTime().ToString('yyyy-MM-dd"T"HH:mm:ss"Z"')) to $($EndDate.ToUniversalTime().ToString('yyyy-MM-dd"T"HH:mm:ss"Z"'))"
Write-Verbose "Updated dates = $($StartDate.ToString('yyyy-MM-dd"T"HH:mm:ss"Z"')) to $($EndDate.ToString('yyyy-MM-dd"T"HH:mm:ss"Z"'))"
}

# Start measuring progress
Expand Down
49 changes: 36 additions & 13 deletions src/powershell/Tests/Unit/Start-FinOpsCostExport.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -59,15 +59,15 @@ Describe 'Start-FinOpsCostExport' {
$params = @{
Name = $exportName
Scope = $scope
StartDate = Get-Date -Month 1 -Day 1 -Hour 0 -Minute 0 -Second 0 -Millisecond 0 -AsUTC
StartDate = [datetime]'2024-01-01'
}

# Act
$success = Start-FinOpsCostExport @params

# Assert
Should -Invoke -ModuleName FinOpsToolkit -CommandName 'Invoke-Rest' -Times 1 `
-ParameterFilter { $body.timePeriod.from -eq $params.StartDate.ToUniversalTime().ToString("yyyy-01-01'T'00:00:00'Z'") -and $body.timePeriod.to -eq $params.StartDate.ToUniversalTime().ToString("yyyy-01-31'T'00:00:00'Z'") }
Should -Invoke -ModuleName FinOpsToolkit -CommandName 'Invoke-Rest' -Times 1 -Exactly `
-ParameterFilter { $body.timePeriod.from -eq '2024-01-01T00:00:00Z' -and $body.timePeriod.to -eq '2024-01-31T00:00:00Z' }
$success | Should -Be $true
}

Expand All @@ -86,8 +86,32 @@ Describe 'Start-FinOpsCostExport' {
$success = Start-FinOpsCostExport @params

# Assert
Should -Invoke -ModuleName FinOpsToolkit -CommandName 'Invoke-Rest' -Times 1 `
-ParameterFilter { $body.timePeriod.from -eq $params.StartDate.ToUniversalTime().Date.ToString("yyyy-MM-dd'T'HH:mm:ss'Z'") -and $body.timePeriod.to -eq $params.EndDate.ToUniversalTime().Date.ToString("yyyy-MM-dd'T'HH:mm:ss'Z'") }
Should -Invoke -ModuleName FinOpsToolkit -CommandName 'Invoke-Rest' -Times 1 -Exactly `
-ParameterFilter { $body.timePeriod.from -eq '2024-06-10T00:00:00Z' -and $body.timePeriod.to -eq '2024-06-20T00:00:00Z' }
$success | Should -Be $true
}

It 'Should use the requested calendar dates regardless of the local time zone' {
# Arrange
Mock -ModuleName FinOpsToolkit -CommandName 'Get-FinOpsCostExport' { $mockExport }
Mock -ModuleName FinOpsToolkit -CommandName 'Invoke-Rest' { @{ Success = $true } }

# Local midnight converts to the previous day in UTC for any positive offset, which used
# to shift the exported period back a day for callers east of UTC. Cover every DateTimeKind
# the parameter binder can produce so the dates are never time zone converted.
$params = @{
Name = $exportName
Scope = $scope
StartDate = [datetime]::SpecifyKind([datetime]'2024-03-01', [DateTimeKind]::Local)
EndDate = [datetime]::SpecifyKind([datetime]'2024-03-31', [DateTimeKind]::Unspecified)
}

# Act
$success = Start-FinOpsCostExport @params

# Assert
Should -Invoke -ModuleName FinOpsToolkit -CommandName 'Invoke-Rest' -Times 1 -Exactly `
-ParameterFilter { $body.timePeriod.from -eq '2024-03-01T00:00:00Z' -and $body.timePeriod.to -eq '2024-03-31T00:00:00Z' }
$success | Should -Be $true
}

Expand All @@ -96,7 +120,7 @@ Describe 'Start-FinOpsCostExport' {
Mock -ModuleName FinOpsToolkit -CommandName 'Get-FinOpsCostExport' { $mockExport }
Mock -ModuleName FinOpsToolkit -CommandName 'Invoke-Rest' { @{ Success = $true } }
$today = (Get-Date).ToUniversalTime().Date
$startOfMonth = (Get-Date -Day 1 -Hour 0 -Minute 0 -Second 0 -Millisecond 0 -AsUTC)
$startOfMonth = $today.AddDays(1 - $today.Day)
$params = @{
Name = $exportName
Scope = $scope
Expand All @@ -107,16 +131,15 @@ Describe 'Start-FinOpsCostExport' {
$success = Start-FinOpsCostExport @params

# Assert
Should -Invoke -ModuleName FinOpsToolkit -CommandName 'Invoke-Rest' -Times ($params.Backfill + 1)
Should -Invoke -ModuleName FinOpsToolkit -CommandName 'Invoke-Rest' -Times 1 -ParameterFilter {
$startDate = $startOfMonth.ToUniversalTime().Date
$body.timePeriod.from -eq $startDate.ToString("yyyy-MM-dd'T'HH:mm:ss'Z'") `
Should -Invoke -ModuleName FinOpsToolkit -CommandName 'Invoke-Rest' -Times ($params.Backfill + 1) -Exactly
Should -Invoke -ModuleName FinOpsToolkit -CommandName 'Invoke-Rest' -Times 1 -Exactly -ParameterFilter {
$body.timePeriod.from -eq $startOfMonth.ToString("yyyy-MM-dd'T'HH:mm:ss'Z'") `
-and $body.timePeriod.to -eq $today.AddDays(-1).ToString("yyyy-MM-dd'T'HH:mm:ss'Z'")
}
foreach ($i in 1..($params.Backfill))
{
Should -Invoke -ModuleName FinOpsToolkit -CommandName 'Invoke-Rest' -Times 1 -ParameterFilter {
$startDate = $startOfMonth.AddMonths($i * -1).ToUniversalTime().Date
Should -Invoke -ModuleName FinOpsToolkit -CommandName 'Invoke-Rest' -Times 1 -Exactly -ParameterFilter {
$startDate = $startOfMonth.AddMonths($i * -1)
$body.timePeriod.from -eq $startDate.ToString("yyyy-MM-dd'T'HH:mm:ss'Z'") `
-and $body.timePeriod.to -eq $startDate.AddMonths(1).AddMilliseconds(-1).ToString("yyyy-MM-dd'T'HH:mm:ss'Z'")
}
Expand Down Expand Up @@ -148,7 +171,7 @@ Describe 'Start-FinOpsCostExport' {

# Set up dates for current month
$today = (Get-Date).ToUniversalTime().Date
$firstDayOfCurrentMonth = (Get-Date -Day 1 -Hour 0 -Minute 0 -Second 0 -Millisecond 0).ToUniversalTime().Date
$firstDayOfCurrentMonth = $today.AddDays(1 - $today.Day)
$lastDayOfCurrentMonth = $firstDayOfCurrentMonth.AddMonths(1).AddDays(-1)

# If testing in the last day of the month, this test might not be relevant
Expand Down
Loading