Add invoices - including Get, List, and GetSummary methods
This commit is contained in:
parent
62d1a5e41e
commit
86ea9fd282
2
godo.go
2
godo.go
|
@ -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}
|
||||
|
|
|
@ -81,6 +81,7 @@ func testClientServices(t *testing.T, c *Client) {
|
|||
"DropletActions",
|
||||
"Images",
|
||||
"ImageActions",
|
||||
"Invoices",
|
||||
"Keys",
|
||||
"Regions",
|
||||
"Sizes",
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue