Merge pull request #301 from rbutler/list-invoices

Add invoices: Get, List, and GetSummary methods
This commit is contained in:
Verónica López 2020-02-18 14:25:56 -05:00 committed by GitHub
commit db948ca548
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 464 additions and 0 deletions

View File

@ -1,5 +1,9 @@
# Change Log
## unreleased
- #301 invoices: Get, Summary, and List methods - @rbutler
## [v1.30.0] - 2020-02-03
- #295 registry: support the created_at field - @adamwg

View File

@ -52,6 +52,7 @@ type Client struct {
DropletActions DropletActionsService
Images ImagesService
ImageActions ImageActionsService
Invoices InvoicesService
Keys KeysService
Regions RegionsService
Sizes SizesService
@ -177,6 +178,7 @@ func NewClient(httpClient *http.Client) *Client {
c.FloatingIPActions = &FloatingIPActionsServiceOp{client: c}
c.Images = &ImagesServiceOp{client: c}
c.ImageActions = &ImageActionsServiceOp{client: c}
c.Invoices = &InvoicesServiceOp{client: c}
c.Keys = &KeysServiceOp{client: c}
c.LoadBalancers = &LoadBalancersServiceOp{client: c}
c.Projects = &ProjectsServiceOp{client: c}

View File

@ -81,6 +81,7 @@ func testClientServices(t *testing.T, c *Client) {
"DropletActions",
"Images",
"ImageActions",
"Invoices",
"Keys",
"Regions",
"Sizes",

186
invoices.go Normal file
View File

@ -0,0 +1,186 @@
package godo
import (
"context"
"fmt"
"net/http"
"time"
)
const invoicesBasePath = "v2/customers/my/invoices"
// InvoicesService is an interface for interfacing with the Invoice
// endpoints of the DigitalOcean API
// See: https://developers.digitalocean.com/documentation/v2/#invoices
type InvoicesService interface {
Get(context.Context, string, *ListOptions) (*Invoice, *Response, error)
List(context.Context, *ListOptions) (*InvoiceList, *Response, error)
GetSummary(context.Context, string) (*InvoiceSummary, *Response, error)
}
// InvoicesServiceOp handles communication with the Invoice related methods of
// the DigitalOcean API.
type InvoicesServiceOp struct {
client *Client
}
var _ InvoicesService = &InvoicesServiceOp{}
// Invoice represents a DigitalOcean Invoice
type Invoice struct {
InvoiceItems []InvoiceItem `json:"invoice_items"`
Links *Links `json:"links"`
Meta *Meta `json:"meta"`
}
// InvoiceItem represents a line-item on a DigitalOcean Invoice
type InvoiceItem struct {
Product string `json:"product"`
ResourceID string `json:"resource_id"`
ResourceUUID string `json:"resource_uuid"`
GroupDescription string `json:"group_description"`
Description string `json:"description"`
Amount string `json:"amount"`
Duration string `json:"duration"`
DurationUnit string `json:"duration_unit"`
StartTime time.Time `json:"start_time"`
EndTime time.Time `json:"end_time"`
ProjectName string `json:"project_name"`
}
// InvoiceList contains a paginated list of all of a customer's invoices.
// The InvoicePreview is the month-to-date usage generated by DigitalOcean.
type InvoiceList struct {
Invoices []InvoiceListItem `json:"invoices"`
InvoicePreview InvoiceListItem `json:"invoice_preview"`
Links *Links `json:"links"`
Meta *Meta `json:"meta"`
}
// InvoiceListItem contains a small list of information about a customer's invoice.
// More information can be found in the Invoice or InvoiceSummary
type InvoiceListItem struct {
InvoiceUUID string `json:"invoice_uuid"`
Amount string `json:"amount"`
InvoicePeriod string `json:"invoice_period"`
UpdatedAt time.Time `json:"updated_at"`
}
// InvoiceSummary contains metadata and summarized usage for an invoice generated by DigitalOcean
type InvoiceSummary struct {
InvoiceUUID string `json:"invoice_uuid"`
BillingPeriod string `json:"billing_period"`
Amount string `json:"amount"`
UserName string `json:"user_name"`
UserBillingAddress Address `json:"user_billing_address"`
UserCompany string `json:"user_company"`
UserEmail string `json:"user_email"`
ProductCharges InvoiceSummaryBreakdown `json:"product_charges"`
Overages InvoiceSummaryBreakdown `json:"overages"`
Taxes InvoiceSummaryBreakdown `json:"taxes"`
CreditsAndAdjustments InvoiceSummaryBreakdown `json:"credits_and_adjustments"`
}
// Address represents the billing address of a customer
type Address struct {
AddressLine1 string `json:"address_line1"`
AddressLine2 string `json:"address_line2"`
City string `json:"city"`
Region string `json:"region"`
PostalCode string `json:"postal_code"`
CountryISO2Code string `json:"country_iso2_code"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// InvoiceSummaryBreakdown is a grouped set of InvoiceItems from an invoice
type InvoiceSummaryBreakdown struct {
Name string `json:"name"`
Amount string `json:"amount"`
Items []InvoiceSummaryBreakdownItem `json:"items"`
}
// InvoiceSummaryBreakdownItem further breaks down the InvoiceSummary by product
type InvoiceSummaryBreakdownItem struct {
Name string `json:"name"`
Amount string `json:"amount"`
Count string `json:"count"`
}
func (i Invoice) String() string {
return Stringify(i)
}
// Get detailed invoice items for an Invoice
func (s *InvoicesServiceOp) Get(ctx context.Context, invoiceUUID string, opt *ListOptions) (*Invoice, *Response, error) {
path := fmt.Sprintf("%s/%s", invoicesBasePath, invoiceUUID)
path, err := addOptions(path, opt)
if err != nil {
return nil, nil, err
}
req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
if err != nil {
return nil, nil, err
}
root := new(Invoice)
resp, err := s.client.Do(ctx, req, root)
if err != nil {
return nil, resp, err
}
if l := root.Links; l != nil {
resp.Links = l
}
if m := root.Meta; m != nil {
resp.Meta = m
}
return root, resp, err
}
// List invoices for a customer
func (s *InvoicesServiceOp) List(ctx context.Context, opt *ListOptions) (*InvoiceList, *Response, error) {
path := invoicesBasePath
path, err := addOptions(path, opt)
if err != nil {
return nil, nil, err
}
req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
if err != nil {
return nil, nil, err
}
root := new(InvoiceList)
resp, err := s.client.Do(ctx, req, root)
if err != nil {
return nil, resp, err
}
if l := root.Links; l != nil {
resp.Links = l
}
if m := root.Meta; m != nil {
resp.Meta = m
}
return root, resp, err
}
// Get a summary of metadata and summarized usage for an Invoice
func (s *InvoicesServiceOp) GetSummary(ctx context.Context, invoiceUUID string) (*InvoiceSummary, *Response, error) {
path := fmt.Sprintf("%s/%s/summary", invoicesBasePath, invoiceUUID)
req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
if err != nil {
return nil, nil, err
}
root := new(InvoiceSummary)
resp, err := s.client.Do(ctx, req, root)
if err != nil {
return nil, resp, err
}
return root, resp, err
}

271
invoices_test.go Normal file
View File

@ -0,0 +1,271 @@
package godo
import (
"fmt"
"net/http"
"reflect"
"testing"
"time"
)
func TestInvoices_GetInvoices(t *testing.T) {
setup()
defer teardown()
mux.HandleFunc("/v2/customers/my/invoices/example-invoice-uuid", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, http.MethodGet)
fmt.Fprint(w, `{
"invoice_items": [
{
"product": "Droplets",
"resource_id": "1234",
"resource_uuid": "droplet-1234-uuid",
"group_description": "",
"description": "My Example Droplet",
"amount": "12.34",
"duration": "672",
"duration_unit": "Hours",
"start_time": "2018-06-20T08:44:38Z",
"end_time": "2018-06-21T08:44:38Z",
"project_name": "My project"
},
{
"product": "Load Balancers",
"resource_id": "2345",
"resource_uuid": "load-balancer-2345-uuid",
"group_description": "",
"description": "My Example Load Balancer",
"amount": "23.45",
"duration": "744",
"duration_unit": "Hours",
"start_time": "2018-06-20T08:44:38Z",
"end_time": "2018-06-21T08:44:38Z",
"project_name": "My Second Project"
}
],
"meta": {
"total": 2
}
}`)
})
invoice, resp, err := client.Invoices.Get(ctx, "example-invoice-uuid", nil)
if err != nil {
t.Errorf("Invoices.Get returned error: %v", err)
}
expectedInvoiceItems := []InvoiceItem{
{
Product: "Droplets",
ResourceID: "1234",
ResourceUUID: "droplet-1234-uuid",
GroupDescription: "",
Description: "My Example Droplet",
Amount: "12.34",
Duration: "672",
DurationUnit: "Hours",
StartTime: time.Date(2018, 6, 20, 8, 44, 38, 0, time.UTC),
EndTime: time.Date(2018, 6, 21, 8, 44, 38, 0, time.UTC),
ProjectName: "My project",
},
{
Product: "Load Balancers",
ResourceID: "2345",
ResourceUUID: "load-balancer-2345-uuid",
GroupDescription: "",
Description: "My Example Load Balancer",
Amount: "23.45",
Duration: "744",
DurationUnit: "Hours",
StartTime: time.Date(2018, 6, 20, 8, 44, 38, 0, time.UTC),
EndTime: time.Date(2018, 6, 21, 8, 44, 38, 0, time.UTC),
ProjectName: "My Second Project",
},
}
actualItems := invoice.InvoiceItems
if !reflect.DeepEqual(actualItems, expectedInvoiceItems) {
t.Errorf("Invoices.Get\nInvoiceItems: got=%#v\nwant=%#v", actualItems, expectedInvoiceItems)
}
expectedMeta := &Meta{Total: 2}
if !reflect.DeepEqual(resp.Meta, expectedMeta) {
t.Errorf("Invoices.Get\nMeta: got=%#v\nwant=%#v", resp.Meta, expectedMeta)
}
}
func TestInvoices_ListInvoices(t *testing.T) {
setup()
defer teardown()
mux.HandleFunc("/v2/customers/my/invoices", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, http.MethodGet)
fmt.Fprint(w, `{
"invoices": [
{
"invoice_uuid": "example-invoice-uuid-1",
"amount": "12.34",
"invoice_period": "2020-01"
},
{
"invoice_uuid": "example-invoice-uuid-2",
"amount": "23.45",
"invoice_period": "2019-12"
}
],
"invoice_preview": {
"invoice_uuid": "example-invoice-uuid-preview",
"amount": "34.56",
"invoice_period": "2020-02",
"updated_at": "2020-02-05T05:43:10Z"
},
"meta": {
"total": 2
}
}`)
})
invoiceListResponse, resp, err := client.Invoices.List(ctx, nil)
if err != nil {
t.Errorf("Invoices.List returned error: %v", err)
}
expectedInvoiceListItems := []InvoiceListItem{
{
InvoiceUUID: "example-invoice-uuid-1",
Amount: "12.34",
InvoicePeriod: "2020-01",
},
{
InvoiceUUID: "example-invoice-uuid-2",
Amount: "23.45",
InvoicePeriod: "2019-12",
},
}
actualItems := invoiceListResponse.Invoices
if !reflect.DeepEqual(actualItems, expectedInvoiceListItems) {
t.Errorf("Invoices.List\nInvoiceListItems: got=%#v\nwant=%#v", actualItems, expectedInvoiceListItems)
}
expectedPreview := InvoiceListItem{
InvoiceUUID: "example-invoice-uuid-preview",
Amount: "34.56",
InvoicePeriod: "2020-02",
UpdatedAt: time.Date(2020, 2, 5, 5, 43, 10, 0, time.UTC),
}
if !reflect.DeepEqual(invoiceListResponse.InvoicePreview, expectedPreview) {
t.Errorf("Invoices.List\nInvoicePreview: got=%#v\nwant=%#v", invoiceListResponse.InvoicePreview, expectedPreview)
}
expectedMeta := &Meta{Total: 2}
if !reflect.DeepEqual(resp.Meta, expectedMeta) {
t.Errorf("Invoices.List\nMeta: got=%#v\nwant=%#v", resp.Meta, expectedMeta)
}
}
func TestInvoices_GetSummary(t *testing.T) {
setup()
defer teardown()
mux.HandleFunc("/v2/customers/my/invoices/example-invoice-uuid/summary", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, http.MethodGet)
fmt.Fprint(w, `{
"invoice_uuid": "example-invoice-uuid",
"billing_period": "2020-01",
"amount": "27.13",
"user_name": "Frodo Baggins",
"user_billing_address": {
"address_line1": "101 Bagshot Row",
"address_line2": "#2",
"city": "Hobbiton",
"region": "Shire",
"postal_code": "12345",
"country_iso2_code": "ME",
"created_at": "2018-06-20T08:44:38Z",
"updated_at": "2018-06-21T08:44:38Z"
},
"user_company": "DigitalOcean",
"user_email": "fbaggins@example.com",
"product_charges": {
"name": "Product usage charges",
"amount": "12.34",
"items": [
{
"amount": "10.00",
"name": "Spaces Subscription",
"count": "1"
},
{
"amount": "2.34",
"name": "Database Clusters",
"count": "1"
}
]
},
"overages": {
"name": "Overages",
"amount": "3.45"
},
"taxes": {
"name": "Taxes",
"amount": "4.56"
},
"credits_and_adjustments": {
"name": "Credits & adjustments",
"amount": "6.78"
}
}`)
})
invoiceSummaryResponse, _, err := client.Invoices.GetSummary(ctx, "example-invoice-uuid")
if err != nil {
t.Errorf("Invoices.GetSummary returned error: %v", err)
}
expectedSummary := InvoiceSummary{
InvoiceUUID: "example-invoice-uuid",
BillingPeriod: "2020-01",
Amount: "27.13",
UserName: "Frodo Baggins",
UserBillingAddress: Address{
AddressLine1: "101 Bagshot Row",
AddressLine2: "#2",
City: "Hobbiton",
Region: "Shire",
PostalCode: "12345",
CountryISO2Code: "ME",
CreatedAt: time.Date(2018, 6, 20, 8, 44, 38, 0, time.UTC),
UpdatedAt: time.Date(2018, 6, 21, 8, 44, 38, 0, time.UTC),
},
UserCompany: "DigitalOcean",
UserEmail: "fbaggins@example.com",
ProductCharges: InvoiceSummaryBreakdown{
Name: "Product usage charges",
Amount: "12.34",
Items: []InvoiceSummaryBreakdownItem{
{
Name: "Spaces Subscription",
Amount: "10.00",
Count: "1",
},
{
Name: "Database Clusters",
Amount: "2.34",
Count: "1",
},
},
},
Overages: InvoiceSummaryBreakdown{
Name: "Overages",
Amount: "3.45",
},
Taxes: InvoiceSummaryBreakdown{
Name: "Taxes",
Amount: "4.56",
},
CreditsAndAdjustments: InvoiceSummaryBreakdown{
Name: "Credits & adjustments",
Amount: "6.78",
},
}
if !reflect.DeepEqual(invoiceSummaryResponse, &expectedSummary) {
t.Errorf("Invoices.GetSummary\nInvoiceSummary: got=%#v\nwant=%#v", invoiceSummaryResponse, &expectedSummary)
}
}