Skip to content

Add new enhanced billing endpoints #3605

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
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
96 changes: 96 additions & 0 deletions github/billing.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,46 @@ type AdvancedSecurityCommittersBreakdown struct {
LastPushedDate *string `json:"last_pushed_date,omitempty"`
}

// UsageReportOptions specifies optional parameters for the enhanced billing platform usage report.
type UsageReportOptions struct {
// If specified, only return results for a single year. The value of year is an integer with four digits representing a year. For example, 2025.
// Default value is the current year.
Year *int `url:"year,omitempty"`

// If specified, only return results for a single month. The value of month is an integer between 1 and 12.
// If no year is specified the default year is used.
Month *int `url:"month,omitempty"`

// If specified, only return results for a single day. The value of day is an integer between 1 and 31.
// If no year or month is specified, the default year and month are used.
Day *int `url:"day,omitempty"`

// If specified, only return results for a single hour. The value of hour is an integer between 0 and 23.
// If no year, month, or day is specified, the default year, month, and day are used.
Hour *int `url:"hour,omitempty"`
}

// UsageItem represents a single usage item in the enhanced billing platform report.
type UsageItem struct {
Date *string `json:"date"`
Product *string `json:"product"`
SKU *string `json:"sku"`
Quantity *int `json:"quantity"`
UnitType *string `json:"unitType"`
PricePerUnit *float64 `json:"pricePerUnit"`
GrossAmount *float64 `json:"grossAmount"`
DiscountAmount *float64 `json:"discountAmount"`
NetAmount *float64 `json:"netAmount"`
RepositoryName *string `json:"repositoryName,omitempty"`
// Organization name is only used for organization-level reports.
OrganizationName *string `json:"organizationName,omitempty"`
}

// UsageReport represents the enhanced billing platform usage report response.
type UsageReport struct {
UsageItems []*UsageItem `json:"usageItems,omitempty"`
}

// GetActionsBillingOrg returns the summary of the free and paid GitHub Actions minutes used for an Org.
//
// GitHub API docs: https://docs.github.com/rest/billing/billing#get-github-actions-billing-for-an-organization
Expand Down Expand Up @@ -216,3 +256,59 @@ func (s *BillingService) GetStorageBillingUser(ctx context.Context, user string)

return storageUserBilling, resp, nil
}

// GetUsageReportOrg returns a report of the total usage for an organization using the enhanced billing platform.
//
// Note: This endpoint is only available to organizations with access to the enhanced billing platform.
//
// GitHub API docs: https://docs.github.com/rest/billing/enhanced-billing#get-billing-usage-report-for-an-organization
//
//meta:operation GET /organizations/{org}/settings/billing/usage
func (s *BillingService) GetUsageReportOrg(ctx context.Context, org string, opts *UsageReportOptions) (*UsageReport, *Response, error) {
u := fmt.Sprintf("organizations/%v/settings/billing/usage", org)
u, err := addOptions(u, opts)
if err != nil {
return nil, nil, err
}

req, err := s.client.NewRequest("GET", u, nil)
if err != nil {
return nil, nil, err
}

usageReport := new(UsageReport)
resp, err := s.client.Do(ctx, req, usageReport)
if err != nil {
return nil, resp, err
}

return usageReport, resp, nil
}

// GetUsageReportUser returns a report of the total usage for a user using the enhanced billing platform.
//
// Note: This endpoint is only available to users with access to the enhanced billing platform.
//
// GitHub API docs: https://docs.github.com/rest/billing/enhanced-billing#get-billing-usage-report-for-a-user
//
//meta:operation GET /users/{username}/settings/billing/usage
func (s *BillingService) GetUsageReportUser(ctx context.Context, user string, opts *UsageReportOptions) (*UsageReport, *Response, error) {
u := fmt.Sprintf("users/%v/settings/billing/usage", user)
u, err := addOptions(u, opts)
if err != nil {
return nil, nil, err
}

req, err := s.client.NewRequest("GET", u, nil)
if err != nil {
return nil, nil, err
}

usageReport := new(UsageReport)
resp, err := s.client.Do(ctx, req, usageReport)
if err != nil {
return nil, resp, err
}

return usageReport, resp, nil
}
164 changes: 164 additions & 0 deletions github/billing_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -510,3 +510,167 @@ func TestBillingService_GetAdvancedSecurityActiveCommittersOrg_invalidOrg(t *tes
_, _, err := client.Billing.GetAdvancedSecurityActiveCommittersOrg(ctx, "%", nil)
testURLParseError(t, err)
}

func TestBillingService_GetUsageReportOrg(t *testing.T) {
t.Parallel()
client, mux, _ := setup(t)

mux.HandleFunc("/organizations/o/settings/billing/usage", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
testFormValues(t, r, values{
"year": "2023",
"month": "8",
})
fmt.Fprint(w, `{
"usageItems": [
{
"date": "2023-08-01",
"product": "Actions",
"sku": "Actions Linux",
"quantity": 100,
"unitType": "minutes",
"pricePerUnit": 0.008,
"grossAmount": 0.8,
"discountAmount": 0,
"netAmount": 0.8,
"organizationName": "GitHub",
"repositoryName": "github/example"
}
]
}`)
})

ctx := context.Background()
opts := &UsageReportOptions{
Year: Ptr(2023),
Month: Ptr(8),
}
report, _, err := client.Billing.GetUsageReportOrg(ctx, "o", opts)
if err != nil {
t.Errorf("Billing.GetUsageReportOrg returned error: %v", err)
}

want := &UsageReport{
UsageItems: []*UsageItem{
{
Date: Ptr("2023-08-01"),
Product: Ptr("Actions"),
SKU: Ptr("Actions Linux"),
Quantity: Ptr(100),
UnitType: Ptr("minutes"),
PricePerUnit: Ptr(0.008),
GrossAmount: Ptr(0.8),
DiscountAmount: Ptr(0.0),
NetAmount: Ptr(0.8),
OrganizationName: Ptr("GitHub"),
RepositoryName: Ptr("github/example"),
},
},
}
if !cmp.Equal(report, want) {
t.Errorf("Billing.GetUsageReportOrg returned %+v, want %+v", report, want)
}

const methodName = "GetUsageReportOrg"
testBadOptions(t, methodName, func() (err error) {
_, _, err = client.Billing.GetUsageReportOrg(ctx, "\n", opts)
return err
})

testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) {
got, resp, err := client.Billing.GetUsageReportOrg(ctx, "o", nil)
if got != nil {
t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got)
}
return resp, err
})
}

func TestBillingService_GetUsageReportOrg_invalidOrg(t *testing.T) {
t.Parallel()
client, _, _ := setup(t)

ctx := context.Background()
_, _, err := client.Billing.GetUsageReportOrg(ctx, "%", nil)
testURLParseError(t, err)
}

func TestBillingService_GetUsageReportUser(t *testing.T) {
t.Parallel()
client, mux, _ := setup(t)

mux.HandleFunc("/users/u/settings/billing/usage", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
testFormValues(t, r, values{
"day": "15",
})
fmt.Fprint(w, `{
"usageItems": [
{
"date": "2023-08-15",
"product": "Codespaces",
"sku": "Codespaces Linux",
"quantity": 50,
"unitType": "hours",
"pricePerUnit": 0.18,
"grossAmount": 9.0,
"discountAmount": 1.0,
"netAmount": 8.0,
"repositoryName": "user/example"
}
]
}`)
})

ctx := context.Background()
opts := &UsageReportOptions{
Day: Ptr(15),
}
report, _, err := client.Billing.GetUsageReportUser(ctx, "u", opts)
if err != nil {
t.Errorf("Billing.GetUsageReportUser returned error: %v", err)
}

want := &UsageReport{
UsageItems: []*UsageItem{
{
Date: Ptr("2023-08-15"),
Product: Ptr("Codespaces"),
SKU: Ptr("Codespaces Linux"),
Quantity: Ptr(50),
UnitType: Ptr("hours"),
PricePerUnit: Ptr(0.18),
GrossAmount: Ptr(9.0),
DiscountAmount: Ptr(1.0),
NetAmount: Ptr(8.0),
RepositoryName: Ptr("user/example"),
},
},
}
if !cmp.Equal(report, want) {
t.Errorf("Billing.GetUsageReportUser returned %+v, want %+v", report, want)
}

const methodName = "GetUsageReportUser"
testBadOptions(t, methodName, func() (err error) {
_, _, err = client.Billing.GetUsageReportUser(ctx, "\n", opts)
return err
})

testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) {
got, resp, err := client.Billing.GetUsageReportUser(ctx, "u", nil)
if got != nil {
t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got)
}
return resp, err
})
}

func TestBillingService_GetUsageReportUser_invalidUser(t *testing.T) {
t.Parallel()
client, _, _ := setup(t)

ctx := context.Background()
_, _, err := client.Billing.GetUsageReportUser(ctx, "%", nil)
testURLParseError(t, err)
}
Loading
Loading