Skip to content

Fix Azure pricesheet billing period fetch for MCA accounts#1

Draft
Copilot wants to merge 2 commits intodevelopfrom
copilot/fix-billing-period-format
Draft

Fix Azure pricesheet billing period fetch for MCA accounts#1
Copilot wants to merge 2 commits intodevelopfrom
copilot/fix-billing-period-format

Conversation

Copy link

Copilot AI commented Feb 20, 2026

The hardcoded yyyyMM billing period format in getDownloadURL only works for EA accounts. MCA accounts use yyyyMM-1 (e.g., 202411-1), causing the Azure API to return a 404 for those users.

Changes

  • pricesheetclient.go: Add GetCurrentBillingPeriod method that calls the Azure Billing Periods List API (/providers/Microsoft.Billing/billingAccounts/{id}/billingPeriods?$top=1) to retrieve the most recent billing period name, handling both EA and MCA name formats dynamically.

  • pricesheetdownloader.go: Update getDownloadURL to call GetCurrentBillingPeriod instead of the hardcoded formatter. Falls back to the original yyyyMM format if the API call fails, preserving backward compatibility.

  • pricesheetclient_test.go: Add validation test for GetCurrentBillingPeriod with empty billing account ID.

// Before: hardcoded, EA-only
poller, err := client.BeginDownloadByBillingPeriod(ctx, currentBillingPeriod()) // "202411"

// After: dynamically fetched, works for EA ("202411") and MCA ("202411-1")
billingPeriod, err := client.GetCurrentBillingPeriod(ctx)
if err != nil {
    log.Warnf("failed to fetch billing period from API, falling back to default format: %s", err)
    billingPeriod = currentBillingPeriod()
}
Original prompt

Problem

GitHub Issue: opencost#2996

The currentBillingPeriod() function in pkg/cloud/azure/pricesheetdownloader.go (line 263) hardcodes the billing period format as yyyyMM (e.g., 202411):

func currentBillingPeriod() string {
	return time.Now().Format("200601")
}

This works for Enterprise Agreement (EA) billing accounts, but Microsoft Customer Agreement (MCA) billing accounts use billing period names with a different format: yyyyMM-1 (e.g., 202411-1). When the hardcoded yyyyMM format is used against an MCA billing account, the Azure API returns:

RESPONSE 404: 404 Not Found
ERROR CODE: 404
{
  "error": {
    "code": "404",
    "message": "Invalid billing period 202411"
  }
}

The user confirmed that their billing periods (listed via az billing period list) are formatted as 202502-1, 202501-1, 202412-1, etc.

Required Fix

Modify getDownloadURL in pkg/cloud/azure/pricesheetdownloader.go to dynamically fetch the current billing period name from the Azure Billing Periods List API instead of using a hardcoded format. Here is the approach:

1. Add a GetCurrentBillingPeriod method to PriceSheetClient in pkg/cloud/azure/pricesheetclient.go

Add these new types and the method after the existing downloadByBillingPeriodCreateRequest function:

// billingPeriodsListTemplate is the URL template for listing billing periods.
const billingPeriodsListTemplate = "/providers/Microsoft.Billing/billingAccounts/%s/billingPeriods"

// billingPeriodsListResponse represents the response from the billing periods list API.
type billingPeriodsListResponse struct {
	Value []billingPeriodEntry `json:"value"`
}

// billingPeriodEntry represents a single billing period entry.
type billingPeriodEntry struct {
	Name string `json:"name"`
}

// GetCurrentBillingPeriod fetches the most recent billing period name from the
// Azure Billing Periods List API. This handles both EA accounts (which use
// "yyyyMM" format) and MCA accounts (which use "yyyyMM-1" format).
// See: https://learn.microsoft.com/en-us/rest/api/billing/billing-periods/list
func (client *PriceSheetClient) GetCurrentBillingPeriod(ctx context.Context) (string, error) {
	if client.billingAccountID == "" {
		return "", errors.New("parameter client.billingAccountID cannot be empty")
	}
	urlPath := fmt.Sprintf(billingPeriodsListTemplate, url.PathEscape(client.billingAccountID))
	req, err := runtime.NewRequest(ctx, http.MethodGet, runtime.JoinPaths(client.host, urlPath))
	if err != nil {
		return "", fmt.Errorf("creating billing periods list request: %w", err)
	}
	reqQP := req.Raw().URL.Query()
	reqQP.Set("api-version", "2020-05-01")
	reqQP.Set("$top", "1")
	req.Raw().URL.RawQuery = reqQP.Encode()
	req.Raw().Header["Accept"] = []string{"application/json"}

	resp, err := client.pl.Do(req)
	if err != nil {
		return "", fmt.Errorf("executing billing periods list request: %w", err)
	}
	if !runtime.HasStatusCode(resp, http.StatusOK) {
		return "", runtime.NewResponseError(resp)
	}

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return "", fmt.Errorf("reading billing periods response body: %w", err)
	}
	defer resp.Body.Close()

	var result billingPeriodsListResponse
	if err := json.Unmarshal(body, &result); err != nil {
		return "", fmt.Errorf("parsing billing periods response: %w", err)
	}

	if len(result.Value) == 0 {
		return "", errors.New("no billing periods returned from API")
	}

	return result.Value[0].Name, nil
}

You will also need to add "encoding/json" and "io" to the imports in pricesheetclient.go.

2. Update getDownloadURL in pkg/cloud/azure/pricesheetdownloader.go

Replace the existing getDownloadURL method (lines 53-73) with:

func (d *PriceSheetDownloader) getDownloadURL(ctx context.Context) (string, error) {
	cred, err := azidentity.NewClientSecretCredential(d.TenantID, d.ClientID, d.ClientSecret, nil)
	if err != nil {
		return "", fmt.Errorf("creating credential: %w", err)
	}
	client, err := NewPriceSheetClient(d.BillingAccount, cred, nil)
	if err != nil {
		return "", fmt.Errorf("creating pricesheet client: %w", err)
	}

	// Dynamically fetch the current billing period name from the API.
	// This handles both EA accounts (yyyyMM) and MCA accounts (yyyyMM-1).
	billingPeriod, err := client.GetCurrentBillingPeriod(ctx)
	if err != nil {
		// Fall back to the hardcoded format for backwards compatibility.
		log.Warnf("failed to fetch billing period from API, falling back to default format: %s", err)
		billingPeriod = currentBillingPeriod()
	}
	log.Infof("using billing period %q", billingPeriod)

	poller, err := client.BeginDownloadByBillingPeriod(ctx, billingPeriod)
	if err != nil {
		return "", fmt.Errorf("beginning pricesheet download: %w", err)
	}
	resp, err := poller.PollUntilDone(ctx, &runtime.PollUntilDoneOptions{
		Frequency: 30 * time.Second,
	})
	if err != nil {
		return "", fmt.Errorf("polling for pricesheet: %w", er...

</details>



<!-- START COPILOT CODING AGENT SUFFIX -->

*This pull request was created from Copilot chat.*
>

<!-- START COPILOT CODING AGENT TIPS -->
---

🔒 GitHub Advanced Security automatically protects Copilot coding agent pull requests. You can protect all pull requests by enabling Advanced Security for your repositories. [Learn more about Advanced Security.](https://gh.io/cca-advanced-security)

Co-authored-by: DonadoJuan <13824533+DonadoJuan@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix billing period format for MCA accounts Fix Azure pricesheet billing period fetch for MCA accounts Feb 20, 2026
Copilot AI requested a review from DonadoJuan February 20, 2026 17:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants