From f857041cd650691b96b784a8e0b553363faef808 Mon Sep 17 00:00:00 2001 From: wpawlik Date: Wed, 2 Oct 2019 12:54:30 +0200 Subject: [PATCH 1/2] support for add_on_usage --- .gitignore | 3 +- add_on_usage.go | 126 +++++++++++++++++ add_on_usage_test.go | 267 +++++++++++++++++++++++++++++++++++++ mock/add_on_usage.go | 50 +++++++ mock/client.go | 2 + pager.go | 37 +++-- recurly.go | 2 + testdata/add_on_usage.xml | 14 ++ testdata/add_on_usages.xml | 16 +++ xml.go | 76 +++++++++++ xml_test.go | 112 ++++++++++++++++ 11 files changed, 692 insertions(+), 13 deletions(-) create mode 100644 add_on_usage.go create mode 100644 add_on_usage_test.go create mode 100644 mock/add_on_usage.go create mode 100644 testdata/add_on_usage.xml create mode 100644 testdata/add_on_usages.xml diff --git a/.gitignore b/.gitignore index 49ce3c1..241fb83 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -/vendor \ No newline at end of file +/vendor +.idea diff --git a/add_on_usage.go b/add_on_usage.go new file mode 100644 index 0000000..3a253fc --- /dev/null +++ b/add_on_usage.go @@ -0,0 +1,126 @@ +package recurly + +import ( + "context" + "encoding/xml" + "fmt" + "net/http" +) + +// AddOnUsageService manages the interactions for add-on usages. +type AddOnUsageService interface { + // List returns a pager to paginate usages for add-on in subscription. PagerOptions are used to + // optionally filter the results. + // + // https://dev.recurly.com/docs/list-add-ons-usage + List(subUUID, addOnCode string, opts *PagerOptions) Pager + + // Create creates a new usage for add-on in subscription. + // + // https://dev.recurly.com/docs/log-usage + Create(ctx context.Context, subUUID, addOnCode string, usage AddOnUsage) (*AddOnUsage, error) + + // Get retrieves an usage. If the usage does not exist, + // a nil usage and nil error are returned. + // + // https://dev.recurly.com/docs/lookup-usage-record + Get(ctx context.Context, subUUID, addOnCode, usageId string) (*AddOnUsage, error) + + // Update updates the usage information. Once usage is billed, only MerchantTag can be updated. + // + // https://dev.recurly.com/docs/update-usage + Update(ctx context.Context, subUUID, addOnCode, usageId string, usage AddOnUsage) (*AddOnUsage, error) + + // Delete removes an usage from subscription add-on. If usage is billed, it can't be removed + // + // https://dev.recurly.com/docs/delete-a-usage-record + Delete(ctx context.Context, subUUID, addOnCode, usageId string) error +} + +// Usage is a billable event or group of events recorded on a purchased usage-based add-on and billed in arrears each billing cycle. +// +// https://dev.recurly.com/docs/usage-record-object +type AddOnUsage struct { + XMLName xml.Name `xml:"usage"` + Id int `xml:"id,omitempty"` + Amount int `xml:"amount,omitempty"` + MerchantTag string `xml:"merchant_tag,omitempty"` + RecordingTimestamp NullTime `xml:"recording_timestamp,omitempty"` + UsageTimestamp NullTime `xml:"usage_timestamp,omitempty"` + CreatedAt NullTime `xml:"created_at,omitempty"` + UpdatedAt NullTime `xml:"updated_at,omitempty"` + BilledAt NullTime `xml:"billed_at,omitempty"` + UsageType string `xml:"usage_type,omitempty"` + UnitAmountInCents int `xml:"unit_amount_in_cents,omitempty"` + UsagePercentage NullFloat `xml:"usage_percentage,omitempty"` +} + +var _ AddOnUsageService = &addOnUsageServiceImpl{} + +type addOnUsageServiceImpl serviceImpl + +func (s *addOnUsageServiceImpl) List(subUUID, addOnCode string, opts *PagerOptions) Pager { + + path := fmt.Sprintf("/subscriptions/%s/add_ons/%s/usage", subUUID, addOnCode) + fmt.Printf("\n huh, path=[%s]\n", path) + return s.client.newPager("GET", path, opts) +} + +func (s *addOnUsageServiceImpl) Get(ctx context.Context, subUUID, addOnCode, usageId string) (*AddOnUsage, error) { + path := fmt.Sprintf("/subscriptions/%s/add_ons/%s/usage/%s", subUUID, addOnCode, usageId) + req, err := s.client.newRequest("GET", path, nil) + if err != nil { + return nil, err + } + + var dst AddOnUsage + if _, err := s.client.do(ctx, req, &dst); err != nil { + if e, ok := err.(*ClientError); ok && e.Response.StatusCode == http.StatusNotFound { + return nil, nil + } + return nil, err + } + return &dst, nil +} + +func (s *addOnUsageServiceImpl) Create(ctx context.Context, subUUID, addOnCode string, usage AddOnUsage) (*AddOnUsage, error) { + + path := fmt.Sprintf("/subscriptions/%s/add_ons/%s/usage", subUUID, addOnCode) + req, err := s.client.newRequest("POST", path, usage) + if err != nil { + return nil, err + } + + var dst AddOnUsage + if _, err := s.client.do(ctx, req, &dst); err != nil { + return nil, err + } + return &dst, nil +} + +func (s *addOnUsageServiceImpl) Update(ctx context.Context, subUUID, addOnCode, usageId string, usage AddOnUsage) (*AddOnUsage, error) { + + path := fmt.Sprintf("/subscriptions/%s/add_ons/%s/usage/%s", subUUID, addOnCode, usageId) + req, err := s.client.newRequest("PUT", path, usage) + if err != nil { + return nil, err + } + + var dst AddOnUsage + if _, err := s.client.do(ctx, req, &dst); err != nil { + return nil, err + } + return &dst, nil +} + +func (s *addOnUsageServiceImpl) Delete(ctx context.Context, subUUID, addOnCode, usageId string) error { + + path := fmt.Sprintf("/subscriptions/%s/add_ons/%s/usage/%s", subUUID, addOnCode, usageId) + req, err := s.client.newRequest("DELETE", path, nil) + if err != nil { + return err + } + + _, err = s.client.do(ctx, req, nil) + return err +} diff --git a/add_on_usage_test.go b/add_on_usage_test.go new file mode 100644 index 0000000..05bdcc7 --- /dev/null +++ b/add_on_usage_test.go @@ -0,0 +1,267 @@ +package recurly_test + +import ( + "bytes" + "context" + "encoding/xml" + "net/http" + "strconv" + "testing" + "time" + + "github.com/blacklightcms/recurly" + "github.com/google/go-cmp/cmp" +) + +// Ensure structs are encoded to XML properly. +func TestAddOnUsage_Encoding(t *testing.T) { + + now := time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC) + tests := []struct { + v recurly.AddOnUsage + expected string + }{ + { + expected: MustCompactString(` + + + `), + }, + { + v: recurly.AddOnUsage{Id: 123456}, + expected: MustCompactString(` + + 123456 + + `), + }, + { + v: recurly.AddOnUsage{Amount: 100}, + expected: MustCompactString(` + + 100 + + `), + }, + { + v: recurly.AddOnUsage{MerchantTag: "some_merchant"}, + expected: MustCompactString(` + + some_merchant + + `), + }, + { + v: recurly.AddOnUsage{RecordingTimestamp: recurly.NewTime(now)}, + expected: MustCompactString(` + + 2000-01-01T00:00:00Z + + `), + }, + { + v: recurly.AddOnUsage{UsageTimestamp: recurly.NewTime(now)}, + expected: MustCompactString(` + + 2000-01-01T00:00:00Z + + `), + }, + { + v: recurly.AddOnUsage{CreatedAt: recurly.NewTime(now)}, + expected: MustCompactString(` + + 2000-01-01T00:00:00Z + + `), + }, + { + v: recurly.AddOnUsage{UpdatedAt: recurly.NewTime(now)}, + expected: MustCompactString(` + + 2000-01-01T00:00:00Z + + `), + }, + { + v: recurly.AddOnUsage{BilledAt: recurly.NewTime(now)}, + expected: MustCompactString(` + + 2000-01-01T00:00:00Z + + `), + }, + { + v: recurly.AddOnUsage{UsageType: "price"}, + expected: MustCompactString(` + + price + + `), + }, + + { + v: recurly.AddOnUsage{UnitAmountInCents: 313}, + expected: MustCompactString(` + + 313 + + `), + }, + { + v: recurly.AddOnUsage{UsagePercentage: recurly.NewFloat(0.50)}, + expected: MustCompactString(` + + 0.5 + + `), + }, + } + + for i, tt := range tests { + t.Run(strconv.Itoa(i), func(t *testing.T) { + buf := new(bytes.Buffer) + if err := xml.NewEncoder(buf).Encode(tt.v); err != nil { + t.Fatal(err) + } else if buf.String() != tt.expected { + t.Fatal(buf.String()) + } + }) + } +} + +func TestAddOnUsage_List(t *testing.T) { + client, s := recurly.NewTestServer() + defer s.Close() + + var invocations int + s.HandleFunc("GET", "/v2/subscriptions/1122334455/add_ons/addOnCode/usage", func(w http.ResponseWriter, r *http.Request) { + invocations++ + w.WriteHeader(http.StatusOK) + w.Write(MustOpenFile("add_on_usages.xml")) + }, t) + + pager := client.AddOnUsages.List("1122334455", "addOnCode", nil) + for pager.Next() { + var a []recurly.AddOnUsage + if err := pager.Fetch(context.Background(), &a); err != nil { + t.Fatal(err) + } else if !s.Invoked { + t.Fatal("expected s to be invoked") + } else if diff := cmp.Diff(a, []recurly.AddOnUsage{*NewTestAddOnUsage()}); diff != "" { + t.Fatal(diff) + } + } + if invocations != 1 { + t.Fatalf("unexpected number of invocations: %d", invocations) + } +} + +func TestAddOnUsage_Get(t *testing.T) { + t.Run("OK", func(t *testing.T) { + client, s := recurly.NewTestServer() + defer s.Close() + + s.HandleFunc("GET", "/v2/subscriptions/1122334455/add_ons/addOnCode/usage/1234", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write(MustOpenFile("add_on_usage.xml")) + }, t) + + if a, err := client.AddOnUsages.Get(context.Background(), "1122334455", "addOnCode", "1234"); err != nil { + t.Fatal(err) + } else if diff := cmp.Diff(a, NewTestAddOnUsage()); diff != "" { + t.Fatal(diff) + } else if !s.Invoked { + t.Fatal("expected fn invocation") + } + }) + + // Ensure a 404 returns nil values. + t.Run("ErrNotFound", func(t *testing.T) { + client, s := recurly.NewTestServer() + defer s.Close() + + s.HandleFunc("GET", "/v2/subscriptions/1122334455/add_ons/addOnCode/usage/8888", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + }, t) + + if a, err := client.AddOnUsages.Get(context.Background(), "1122334455", "addOnCode", "8888"); !s.Invoked { + t.Fatal("expected fn invocation") + } else if err != nil { + t.Fatal(err) + } else if a != nil { + t.Fatalf("expected nil: %#v", a) + } + }) +} + +func TestAddOnUsage_Create(t *testing.T) { + client, s := recurly.NewTestServer() + defer s.Close() + + s.HandleFunc("POST", "/v2/subscriptions/1122334455/add_ons/addOnCode/usage", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusCreated) + w.Write(MustOpenFile("add_on_usage.xml")) + }, t) + + if a, err := client.AddOnUsages.Create(context.Background(), "1122334455", "addOnCode", recurly.AddOnUsage{}); !s.Invoked { + t.Fatal("expected fn invocation") + } else if err != nil { + t.Fatal(err) + } else if diff := cmp.Diff(a, NewTestAddOnUsage()); diff != "" { + t.Fatal(diff) + } +} + +func TestAddOnUsage_Update(t *testing.T) { + client, s := recurly.NewTestServer() + defer s.Close() + + s.HandleFunc("PUT", "/v2/subscriptions/1122334455/add_ons/addOnCode/usage/1234", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write(MustOpenFile("add_on_usage.xml")) + }, t) + + if a, err := client.AddOnUsages.Update(context.Background(), "1122334455", "addOnCode", "1234", recurly.AddOnUsage{}); !s.Invoked { + t.Fatal("expected fn invocation") + } else if err != nil { + t.Fatal(err) + } else if diff := cmp.Diff(a, NewTestAddOnUsage()); diff != "" { + t.Fatal(diff) + } +} + +func TestAddOnUsage_Delete(t *testing.T) { + client, s := recurly.NewTestServer() + defer s.Close() + + s.HandleFunc("DELETE", "/v2/subscriptions/1122334455/add_ons/addOnCode/usage/1234", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNoContent) + }, t) + + if err := client.AddOnUsages.Delete(context.Background(), "1122334455", "addOnCode", "1234"); !s.Invoked { + t.Fatal("expected fn invocation") + } else if err != nil { + t.Fatal(err) + } +} + + +// Returns add on corresponding to testdata/add_on.xml +func NewTestAddOnUsage() *recurly.AddOnUsage { + + now := time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC) + return &recurly.AddOnUsage{ + XMLName: xml.Name{Local: "usage"}, + Amount: 1, + MerchantTag: "Order ID: 4939853977878713", + RecordingTimestamp: recurly.NewTime(now), + UsageTimestamp: recurly.NewTime(now), + CreatedAt: recurly.NewTime(now), + UpdatedAt: recurly.NullTime{}, + BilledAt: recurly.NullTime{}, + UsageType: "price", + UnitAmountInCents: 45, + UsagePercentage: recurly.NullFloat{}, + } +} \ No newline at end of file diff --git a/mock/add_on_usage.go b/mock/add_on_usage.go new file mode 100644 index 0000000..5b00374 --- /dev/null +++ b/mock/add_on_usage.go @@ -0,0 +1,50 @@ +package mock + +import ( + "context" + "github.com/blacklightcms/recurly" +) + +var _ recurly.AddOnUsageService = &AddOnUsageService{} + +type AddOnUsageService struct { + OnList func(subUUID, addOnCode string, opts *recurly.PagerOptions) recurly.Pager + ListInvoked bool + + OnGet func(ctx context.Context, subUUID, addOnCode, usageId string) (*recurly.AddOnUsage, error) + GetInvoked bool + + OnCreate func(ctx context.Context, subUUID, addOnCode string, usage recurly.AddOnUsage) (*recurly.AddOnUsage, error) + CreateInvoked bool + + OnUpdate func(ctx context.Context, subUUID, addOnCode string, usage recurly.AddOnUsage) (*recurly.AddOnUsage, error) + UpdateInvoked bool + + OnDelete func(ctx context.Context, subUUID, addOnCode, usageId string) error + DeleteInvoked bool +} + +func (m *AddOnUsageService) List(subUUID, addOnCode string, opts *recurly.PagerOptions) recurly.Pager { + m.ListInvoked = true + return m.OnList(subUUID, addOnCode, opts) +} + +func (m *AddOnUsageService) Get(ctx context.Context, subUUID, addOnCode, usageId string) (*recurly.AddOnUsage, error) { + m.GetInvoked = true + return m.OnGet(ctx, subUUID, addOnCode, usageId) +} + +func (m *AddOnUsageService) Create(ctx context.Context, subUUID, addOnCode string, usage recurly.AddOnUsage) (*recurly.AddOnUsage, error) { + m.CreateInvoked = true + return m.OnCreate(ctx, subUUID, addOnCode, usage) +} + +func (m *AddOnUsageService) Update(ctx context.Context, subUUID, addOnCode, usageId string, usage recurly.AddOnUsage) (*recurly.AddOnUsage, error) { + m.UpdateInvoked = true + return m.OnUpdate(ctx, subUUID, addOnCode, usage) +} + +func (m *AddOnUsageService) Delete(ctx context.Context, subUUID, addOnCode, usageId string) error { + m.DeleteInvoked = true + return m.OnDelete(ctx, subUUID, addOnCode, usageId) +} diff --git a/mock/client.go b/mock/client.go index bfe570a..efe28b8 100644 --- a/mock/client.go +++ b/mock/client.go @@ -11,6 +11,7 @@ type Client struct { Accounts AccountsService AddOns AddOnsService + AddOnUsages AddOnUsageService Adjustments AdjustmentsService Billing BillingService Coupons CouponsService @@ -33,6 +34,7 @@ func NewClient(subdomain, apiKey string) *Client { // Attach mock implementations. c.Client.Accounts = &c.Accounts c.Client.AddOns = &c.AddOns + c.Client.AddOnUsages = &c.AddOnUsages c.Client.Adjustments = &c.Adjustments c.Client.Billing = &c.Billing c.Client.Coupons = &c.Coupons diff --git a/pager.go b/pager.go index 067eb7d..065837e 100644 --- a/pager.go +++ b/pager.go @@ -116,6 +116,7 @@ func (p *pager) Fetch(ctx context.Context, dst interface{}) error { Account []Account `xml:"account"` Adjustment []Adjustment `xml:"adjustment"` AddOn []AddOn `xml:"add_on"` + AddOnUsage []AddOnUsage `xml:"usage"` Coupon []Coupon `xml:"coupon"` CreditPayment []CreditPayment `xml:"credit_payment"` Invoice []Invoice `xml:"invoice"` @@ -144,6 +145,8 @@ func (p *pager) Fetch(ctx context.Context, dst interface{}) error { *v = unmarshaler.Adjustment case *[]AddOn: *v = unmarshaler.AddOn + case *[]AddOnUsage: + *v = unmarshaler.AddOnUsage case *[]Coupon: *v = unmarshaler.Coupon case *[]CreditPayment: @@ -207,6 +210,16 @@ func (p *pager) FetchAll(ctx context.Context, dst interface{}) error { all = append(all, dst...) } *v = all + case *[]AddOnUsage: + var all []AddOnUsage + for p.Next() { + var dst []AddOnUsage + if err := p.Fetch(ctx, &dst); err != nil { + return err + } + all = append(all, dst...) + } + *v = all case *[]Coupon: var all []Coupon for p.Next() { @@ -339,7 +352,7 @@ type PagerOptions struct { // query is for any one-off URL params used by a specific endpoint. // Values sent as time.Time or recurly.NullTime will be automatically // converted to a valid datetime format for Recurly. - query query + Query query // Cursor is set internally by the library. If you are paginating // records non-consecutively and obtained the next cursor, you can set it @@ -384,19 +397,19 @@ func (q query) append(u *url.URL) { // append appends params to a URL. func (p PagerOptions) append(u *url.URL) { - if p.query == nil { - p.query = map[string]interface{}{} + if p.Query == nil { + p.Query = map[string]interface{}{} } if p.PerPage > 0 { - p.query["per_page"] = p.PerPage + p.Query["per_page"] = p.PerPage } - p.query["begin_time"] = p.BeginTime.String() - p.query["end_time"] = p.EndTime.String() - p.query["sort"] = p.Sort - p.query["order"] = p.Order - p.query["state"] = p.State - p.query["type"] = p.Type - p.query["cursor"] = p.Cursor - p.query.append(u) + p.Query["begin_time"] = p.BeginTime.String() + p.Query["end_time"] = p.EndTime.String() + p.Query["sort"] = p.Sort + p.Query["order"] = p.Order + p.Query["state"] = p.State + p.Query["type"] = p.Type + p.Query["cursor"] = p.Cursor + p.Query.append(u) } diff --git a/recurly.go b/recurly.go index ec1e499..98d72b9 100644 --- a/recurly.go +++ b/recurly.go @@ -54,6 +54,7 @@ type Client struct { Accounts AccountsService Adjustments AdjustmentsService AddOns AddOnsService + AddOnUsages AddOnUsageService Billing BillingService Coupons CouponsService CreditPayments CreditPaymentsService @@ -95,6 +96,7 @@ func NewClient(subdomain, apiKey string) *Client { client.Accounts = &accountsImpl{client: client} client.Adjustments = &adjustmentsImpl{client: client} client.AddOns = &addOnsImpl{client: client} + client.AddOnUsages = &addOnUsageServiceImpl{client: client} client.Billing = &billingImpl{client: client} client.Coupons = &couponsImpl{client: client} client.CreditPayments = &creditInvoicesImpl{client: client} diff --git a/testdata/add_on_usage.xml b/testdata/add_on_usage.xml new file mode 100644 index 0000000..c3a2df0 --- /dev/null +++ b/testdata/add_on_usage.xml @@ -0,0 +1,14 @@ + + + + 1 + Order ID: 4939853977878713 + 2000-01-01T00:00:00Z + 2000-01-01T00:00:00Z + 2000-01-01T00:00:00Z + + + price + 45 + + \ No newline at end of file diff --git a/testdata/add_on_usages.xml b/testdata/add_on_usages.xml new file mode 100644 index 0000000..5968ec5 --- /dev/null +++ b/testdata/add_on_usages.xml @@ -0,0 +1,16 @@ + + + + + 1 + Order ID: 4939853977878713 + 2000-01-01T00:00:00Z + 2000-01-01T00:00:00Z + 2000-01-01T00:00:00Z + + + price + 45 + + + \ No newline at end of file diff --git a/xml.go b/xml.go index d528272..d049ae1 100644 --- a/xml.go +++ b/xml.go @@ -164,6 +164,82 @@ func (n NullInt) MarshalXML(e *xml.Encoder, start xml.StartElement) error { return nil } +// NullFloat is used for properly handling float types that could be null. (float64 is returned) +type NullFloat struct { + value float64 + valid bool +} + +// NullFloat returns NullFloat with a valid value of f. +func NewFloat(f float64) NullFloat { + return NullFloat{value: f, valid: true} +} + +// NewFloatPtr returns a new NullFloat from a pointer to float64. +func NewFloatPtr(f *float64) NullFloat { + if f == nil { + return NullFloat{} + } + return NewFloat(*f) +} + +// Float64 returns the float64 value, regardless of validity. Use Value() if +// you need to know whether the value is valid. +func (n NullFloat) Float64() float64 { + return n.value +} + +// Float64Ptr returns a pointer to the float64 value, or nil if the value is not valid. +func (n NullFloat) Float64Ptr() *float64 { + if n.valid { + return &n.value + } + return nil +} + +// Value returns the value of NullFloat. The value should only be considered +// valid if ok returns true. +func (n NullFloat) Value() (value float64, ok bool) { + return n.value, n.valid +} + +// Equal compares the equality of two NullFloat. +func (n NullFloat) Equal(v NullFloat) bool { + return n.value == v.value && n.valid == v.valid +} + +// MarshalJSON marshals an float64 based on whether valid is true. +func (n NullFloat) MarshalJSON() ([]byte, error) { + if n.valid { + return json.Marshal(n.value) + } + return []byte("null"), nil +} + +// UnmarshalXML unmarshals an float64 properly, as well as marshaling an empty string to nil. +func (n *NullFloat) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + var v struct { + Float float64 `xml:",chardata"` + Nil string `xml:"nil,attr"` + } + if err := d.DecodeElement(&v, &start); err != nil { + return err + } else if strings.EqualFold(v.Nil, "nil") || strings.EqualFold(v.Nil, "true") { + return nil + } + *n = NewFloat(v.Float) + return nil +} + +// MarshalXML marshals NullFloat greater than zero to XML. Otherwise nothing is +// marshaled. +func (n NullFloat) MarshalXML(e *xml.Encoder, start xml.StartElement) error { + if n.valid { + return e.EncodeElement(n.value, start) + } + return nil +} + // DateTimeFormat is the format Recurly uses to represent datetimes. const DateTimeFormat = "2006-01-02T15:04:05Z07:00" diff --git a/xml_test.go b/xml_test.go index 8fa28ba..c2aa0c1 100644 --- a/xml_test.go +++ b/xml_test.go @@ -233,6 +233,118 @@ func TestXML_NullIntPtr(t *testing.T) { } } +func TestXML_NullFloat(t *testing.T) { + t.Run("ZeroValue", func(t *testing.T) { + var f recurly.NullFloat + if value, ok := f.Value(); ok { + t.Fatal("expected ok to be false") + } else if value != 0 { + t.Fatalf("unexpected value: %f", value) + } + + if f.Float64() != 0 { + t.Fatalf("unexpected value: %f", f.Float64()) + } else if ptr := f.Float64Ptr(); ptr != nil { + t.Fatalf("expected nil: %#v", ptr) + } + }) + + f := recurly.NewFloat(12.34) + if value, ok := f.Value(); !ok { + t.Fatal("expected ok to be true") + } else if value != 12.34 { + t.Fatalf("unexpected value: %f", value) + } + + if f.Float64() != 12.34 { + t.Fatalf("unexpected value: %f", f.Float64()) + } else if ptr := f.Float64Ptr(); ptr == nil { + t.Fatal("expected non-nil value") + } else if *ptr != 12.34 { + t.Fatalf("unexpected value: %#v", ptr) + } + + f = recurly.NewFloat(0.00) + if value, ok := f.Value(); !ok { + t.Fatal("expected ok to be true") + } else if value != 0 { + t.Fatalf("unexpected value: %f", value) + } + + if f.Float64() != 0 { + t.Fatalf("unexpected value: %f", f.Float64()) + } else if ptr := f.Float64Ptr(); ptr == nil { + t.Fatal("expected non-nil value") + } else if *ptr != 0 { + t.Fatalf("unexpected value: %#v", ptr) + } + + type testStruct struct { + XMLName xml.Name `xml:"test"` + Value recurly.NullFloat `xml:"f"` + } + + t.Run("Encode", func(t *testing.T) { + for i, tt := range []struct { + value recurly.NullFloat + expect string + }{ + {value: recurly.NewFloat(12.34), expect: `12.34`}, + {value: recurly.NewFloat(0.0000), expect: `0`}, + {value: recurly.NewFloat(-12.34), expect: `-12.34`}, + {value: recurly.NewFloat(-0.01), expect: `-0.01`}, + {value: recurly.NewFloat(0.009), expect: `0.009`}, + {expect: ``}, // zero value + } { + if aXml, err := xml.Marshal(testStruct{Value: tt.value}); err != nil { + t.Fatalf("%d %#v", i, err) + } else if string(aXml) != tt.expect { + t.Fatalf("%d %s", i, string(aXml)) + } + } + }) + + t.Run("Decode", func(t *testing.T) { + for i, tt := range []struct { + expect recurly.NullFloat + input string + }{ + {expect: recurly.NewFloat(12.34), input: `12.34`}, + {expect: recurly.NewFloat(0), input: `0`}, + {expect: recurly.NewFloat(-12.34), input: `-12.34`}, + {expect: recurly.NewFloat(-0.01), input: `-0.01`}, + {expect: recurly.NewFloat(0.009), input: `0.009`}, + {input: ``}, // zero value + } { + var dst testStruct + if err := xml.Unmarshal([]byte(tt.input), &dst); err != nil { + t.Fatalf("%d %#v", i, err) + } else if diff := cmp.Diff(testStruct{XMLName: xml.Name{Local: "test"}, Value: tt.expect}, dst); diff != "" { + t.Fatalf("%d %s", i, diff) + } + } + }) +} + +func TestXML_NullFloatPtr(t *testing.T) { + f := recurly.NewFloatPtr(nil) + if value, ok := f.Value(); ok { + t.Fatal("expected ok to be false") + } else if value != 0 { + t.Fatalf("unexpected value: %f", value) + } + + floatVal := 0.07 + f = recurly.NewFloatPtr(&floatVal) + if value, ok := f.Value(); !ok { + t.Fatal("expected ok to be true") + } else if value != 0.07 { + t.Fatalf("unexpected value: %f", value) + } +} + + + func TestXML_NullTime(t *testing.T) { t.Run("ZeroValue", func(t *testing.T) { var rt recurly.NullTime From a472d0aefdd680ffd1d9d2b52f0e7b9b54f50cf2 Mon Sep 17 00:00:00 2001 From: wpawlik Date: Wed, 16 Oct 2019 15:33:52 +0200 Subject: [PATCH 2/2] post PR fixes (https://github.com/blacklightcms/recurly/pull/130) --- .gitignore | 1 - add_on_usage.go | 59 +++++++++++++++++--------------------- add_on_usage_test.go | 9 ++---- mock/add_on_usage.go | 30 +++++++++---------- pager.go | 2 +- testdata/add_on_usage.xml | 2 +- testdata/add_on_usages.xml | 2 +- xml_test.go | 4 +-- 8 files changed, 49 insertions(+), 60 deletions(-) diff --git a/.gitignore b/.gitignore index 241fb83..61ead86 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1 @@ /vendor -.idea diff --git a/add_on_usage.go b/add_on_usage.go index 3a253fc..6e6ff21 100644 --- a/add_on_usage.go +++ b/add_on_usage.go @@ -13,61 +13,59 @@ type AddOnUsageService interface { // optionally filter the results. // // https://dev.recurly.com/docs/list-add-ons-usage - List(subUUID, addOnCode string, opts *PagerOptions) Pager + List(uuid, addOnCode string, opts *PagerOptions) Pager // Create creates a new usage for add-on in subscription. // // https://dev.recurly.com/docs/log-usage - Create(ctx context.Context, subUUID, addOnCode string, usage AddOnUsage) (*AddOnUsage, error) + Create(ctx context.Context, uuid, addOnCode string, usage AddOnUsage) (*AddOnUsage, error) // Get retrieves an usage. If the usage does not exist, // a nil usage and nil error are returned. // // https://dev.recurly.com/docs/lookup-usage-record - Get(ctx context.Context, subUUID, addOnCode, usageId string) (*AddOnUsage, error) + Get(ctx context.Context, uuid, addOnCode, usageID string) (*AddOnUsage, error) // Update updates the usage information. Once usage is billed, only MerchantTag can be updated. // // https://dev.recurly.com/docs/update-usage - Update(ctx context.Context, subUUID, addOnCode, usageId string, usage AddOnUsage) (*AddOnUsage, error) + Update(ctx context.Context, uuid, addOnCode, usageID string, usage AddOnUsage) (*AddOnUsage, error) // Delete removes an usage from subscription add-on. If usage is billed, it can't be removed // // https://dev.recurly.com/docs/delete-a-usage-record - Delete(ctx context.Context, subUUID, addOnCode, usageId string) error + Delete(ctx context.Context, uuid, addOnCode, usageID string) error } // Usage is a billable event or group of events recorded on a purchased usage-based add-on and billed in arrears each billing cycle. // // https://dev.recurly.com/docs/usage-record-object type AddOnUsage struct { - XMLName xml.Name `xml:"usage"` - Id int `xml:"id,omitempty"` - Amount int `xml:"amount,omitempty"` - MerchantTag string `xml:"merchant_tag,omitempty"` - RecordingTimestamp NullTime `xml:"recording_timestamp,omitempty"` - UsageTimestamp NullTime `xml:"usage_timestamp,omitempty"` - CreatedAt NullTime `xml:"created_at,omitempty"` - UpdatedAt NullTime `xml:"updated_at,omitempty"` - BilledAt NullTime `xml:"billed_at,omitempty"` - UsageType string `xml:"usage_type,omitempty"` - UnitAmountInCents int `xml:"unit_amount_in_cents,omitempty"` - UsagePercentage NullFloat `xml:"usage_percentage,omitempty"` + XMLName xml.Name `xml:"usage"` + ID int `xml:"id,omitempty"` + Amount int `xml:"amount,omitempty"` + MerchantTag string `xml:"merchant_tag,omitempty"` + RecordingTimestamp NullTime `xml:"recording_timestamp,omitempty"` + UsageTimestamp NullTime `xml:"usage_timestamp,omitempty"` + CreatedAt NullTime `xml:"created_at,omitempty"` + UpdatedAt NullTime `xml:"updated_at,omitempty"` + BilledAt NullTime `xml:"billed_at,omitempty"` + UsageType string `xml:"usage_type,omitempty"` + UnitAmountInCents int `xml:"unit_amount_in_cents,omitempty"` + UsagePercentage NullFloat `xml:"usage_percentage,omitempty"` } var _ AddOnUsageService = &addOnUsageServiceImpl{} type addOnUsageServiceImpl serviceImpl -func (s *addOnUsageServiceImpl) List(subUUID, addOnCode string, opts *PagerOptions) Pager { - - path := fmt.Sprintf("/subscriptions/%s/add_ons/%s/usage", subUUID, addOnCode) - fmt.Printf("\n huh, path=[%s]\n", path) +func (s *addOnUsageServiceImpl) List(uuid, addOnCode string, opts *PagerOptions) Pager { + path := fmt.Sprintf("/subscriptions/%s/add_ons/%s/usage", uuid, addOnCode) return s.client.newPager("GET", path, opts) } -func (s *addOnUsageServiceImpl) Get(ctx context.Context, subUUID, addOnCode, usageId string) (*AddOnUsage, error) { - path := fmt.Sprintf("/subscriptions/%s/add_ons/%s/usage/%s", subUUID, addOnCode, usageId) +func (s *addOnUsageServiceImpl) Get(ctx context.Context, uuid, addOnCode, usageID string) (*AddOnUsage, error) { + path := fmt.Sprintf("/subscriptions/%s/add_ons/%s/usage/%s", uuid, addOnCode, usageID) req, err := s.client.newRequest("GET", path, nil) if err != nil { return nil, err @@ -83,9 +81,8 @@ func (s *addOnUsageServiceImpl) Get(ctx context.Context, subUUID, addOnCode, usa return &dst, nil } -func (s *addOnUsageServiceImpl) Create(ctx context.Context, subUUID, addOnCode string, usage AddOnUsage) (*AddOnUsage, error) { - - path := fmt.Sprintf("/subscriptions/%s/add_ons/%s/usage", subUUID, addOnCode) +func (s *addOnUsageServiceImpl) Create(ctx context.Context, uuid, addOnCode string, usage AddOnUsage) (*AddOnUsage, error) { + path := fmt.Sprintf("/subscriptions/%s/add_ons/%s/usage", uuid, addOnCode) req, err := s.client.newRequest("POST", path, usage) if err != nil { return nil, err @@ -98,9 +95,8 @@ func (s *addOnUsageServiceImpl) Create(ctx context.Context, subUUID, addOnCode s return &dst, nil } -func (s *addOnUsageServiceImpl) Update(ctx context.Context, subUUID, addOnCode, usageId string, usage AddOnUsage) (*AddOnUsage, error) { - - path := fmt.Sprintf("/subscriptions/%s/add_ons/%s/usage/%s", subUUID, addOnCode, usageId) +func (s *addOnUsageServiceImpl) Update(ctx context.Context, uuid, addOnCode, usageID string, usage AddOnUsage) (*AddOnUsage, error) { + path := fmt.Sprintf("/subscriptions/%s/add_ons/%s/usage/%s", uuid, addOnCode, usageID) req, err := s.client.newRequest("PUT", path, usage) if err != nil { return nil, err @@ -113,9 +109,8 @@ func (s *addOnUsageServiceImpl) Update(ctx context.Context, subUUID, addOnCode, return &dst, nil } -func (s *addOnUsageServiceImpl) Delete(ctx context.Context, subUUID, addOnCode, usageId string) error { - - path := fmt.Sprintf("/subscriptions/%s/add_ons/%s/usage/%s", subUUID, addOnCode, usageId) +func (s *addOnUsageServiceImpl) Delete(ctx context.Context, uuid, addOnCode, usageID string) error { + path := fmt.Sprintf("/subscriptions/%s/add_ons/%s/usage/%s", uuid, addOnCode, usageID) req, err := s.client.newRequest("DELETE", path, nil) if err != nil { return err diff --git a/add_on_usage_test.go b/add_on_usage_test.go index 05bdcc7..b8f1c1c 100644 --- a/add_on_usage_test.go +++ b/add_on_usage_test.go @@ -15,7 +15,6 @@ import ( // Ensure structs are encoded to XML properly. func TestAddOnUsage_Encoding(t *testing.T) { - now := time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC) tests := []struct { v recurly.AddOnUsage @@ -28,7 +27,7 @@ func TestAddOnUsage_Encoding(t *testing.T) { `), }, { - v: recurly.AddOnUsage{Id: 123456}, + v: recurly.AddOnUsage{ID: 123456}, expected: MustCompactString(` 123456 @@ -246,10 +245,8 @@ func TestAddOnUsage_Delete(t *testing.T) { } } - // Returns add on corresponding to testdata/add_on.xml func NewTestAddOnUsage() *recurly.AddOnUsage { - now := time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC) return &recurly.AddOnUsage{ XMLName: xml.Name{Local: "usage"}, @@ -262,6 +259,6 @@ func NewTestAddOnUsage() *recurly.AddOnUsage { BilledAt: recurly.NullTime{}, UsageType: "price", UnitAmountInCents: 45, - UsagePercentage: recurly.NullFloat{}, + UsagePercentage: recurly.NewFloat(12.34), } -} \ No newline at end of file +} diff --git a/mock/add_on_usage.go b/mock/add_on_usage.go index 5b00374..7f6f3ff 100644 --- a/mock/add_on_usage.go +++ b/mock/add_on_usage.go @@ -8,43 +8,43 @@ import ( var _ recurly.AddOnUsageService = &AddOnUsageService{} type AddOnUsageService struct { - OnList func(subUUID, addOnCode string, opts *recurly.PagerOptions) recurly.Pager + OnList func(uuid, addOnCode string, opts *recurly.PagerOptions) recurly.Pager ListInvoked bool - OnGet func(ctx context.Context, subUUID, addOnCode, usageId string) (*recurly.AddOnUsage, error) + OnGet func(ctx context.Context, uuid, addOnCode, usageId string) (*recurly.AddOnUsage, error) GetInvoked bool - OnCreate func(ctx context.Context, subUUID, addOnCode string, usage recurly.AddOnUsage) (*recurly.AddOnUsage, error) + OnCreate func(ctx context.Context, uuid, addOnCode string, usage recurly.AddOnUsage) (*recurly.AddOnUsage, error) CreateInvoked bool - OnUpdate func(ctx context.Context, subUUID, addOnCode string, usage recurly.AddOnUsage) (*recurly.AddOnUsage, error) + OnUpdate func(ctx context.Context, uuid, addOnCode string, usage recurly.AddOnUsage) (*recurly.AddOnUsage, error) UpdateInvoked bool - OnDelete func(ctx context.Context, subUUID, addOnCode, usageId string) error + OnDelete func(ctx context.Context, uuid, addOnCode, usageId string) error DeleteInvoked bool } -func (m *AddOnUsageService) List(subUUID, addOnCode string, opts *recurly.PagerOptions) recurly.Pager { +func (m *AddOnUsageService) List(uuid, addOnCode string, opts *recurly.PagerOptions) recurly.Pager { m.ListInvoked = true - return m.OnList(subUUID, addOnCode, opts) + return m.OnList(uuid, addOnCode, opts) } -func (m *AddOnUsageService) Get(ctx context.Context, subUUID, addOnCode, usageId string) (*recurly.AddOnUsage, error) { +func (m *AddOnUsageService) Get(ctx context.Context, uuid, addOnCode, usageId string) (*recurly.AddOnUsage, error) { m.GetInvoked = true - return m.OnGet(ctx, subUUID, addOnCode, usageId) + return m.OnGet(ctx, uuid, addOnCode, usageId) } -func (m *AddOnUsageService) Create(ctx context.Context, subUUID, addOnCode string, usage recurly.AddOnUsage) (*recurly.AddOnUsage, error) { +func (m *AddOnUsageService) Create(ctx context.Context, uuid, addOnCode string, usage recurly.AddOnUsage) (*recurly.AddOnUsage, error) { m.CreateInvoked = true - return m.OnCreate(ctx, subUUID, addOnCode, usage) + return m.OnCreate(ctx, uuid, addOnCode, usage) } -func (m *AddOnUsageService) Update(ctx context.Context, subUUID, addOnCode, usageId string, usage recurly.AddOnUsage) (*recurly.AddOnUsage, error) { +func (m *AddOnUsageService) Update(ctx context.Context, uuid, addOnCode, usageId string, usage recurly.AddOnUsage) (*recurly.AddOnUsage, error) { m.UpdateInvoked = true - return m.OnUpdate(ctx, subUUID, addOnCode, usage) + return m.OnUpdate(ctx, uuid, addOnCode, usage) } -func (m *AddOnUsageService) Delete(ctx context.Context, subUUID, addOnCode, usageId string) error { +func (m *AddOnUsageService) Delete(ctx context.Context, uuid, addOnCode, usageId string) error { m.DeleteInvoked = true - return m.OnDelete(ctx, subUUID, addOnCode, usageId) + return m.OnDelete(ctx, uuid, addOnCode, usageId) } diff --git a/pager.go b/pager.go index 065837e..63ebdb6 100644 --- a/pager.go +++ b/pager.go @@ -116,7 +116,7 @@ func (p *pager) Fetch(ctx context.Context, dst interface{}) error { Account []Account `xml:"account"` Adjustment []Adjustment `xml:"adjustment"` AddOn []AddOn `xml:"add_on"` - AddOnUsage []AddOnUsage `xml:"usage"` + AddOnUsage []AddOnUsage `xml:"usage"` Coupon []Coupon `xml:"coupon"` CreditPayment []CreditPayment `xml:"credit_payment"` Invoice []Invoice `xml:"invoice"` diff --git a/testdata/add_on_usage.xml b/testdata/add_on_usage.xml index c3a2df0..950fb4c 100644 --- a/testdata/add_on_usage.xml +++ b/testdata/add_on_usage.xml @@ -10,5 +10,5 @@ price 45 - + 12.34 \ No newline at end of file diff --git a/testdata/add_on_usages.xml b/testdata/add_on_usages.xml index 5968ec5..671d242 100644 --- a/testdata/add_on_usages.xml +++ b/testdata/add_on_usages.xml @@ -11,6 +11,6 @@ price 45 - + 12.34 \ No newline at end of file diff --git a/xml_test.go b/xml_test.go index c2aa0c1..5f87d06 100644 --- a/xml_test.go +++ b/xml_test.go @@ -280,7 +280,7 @@ func TestXML_NullFloat(t *testing.T) { } type testStruct struct { - XMLName xml.Name `xml:"test"` + XMLName xml.Name `xml:"test"` Value recurly.NullFloat `xml:"f"` } @@ -343,8 +343,6 @@ func TestXML_NullFloatPtr(t *testing.T) { } } - - func TestXML_NullTime(t *testing.T) { t.Run("ZeroValue", func(t *testing.T) { var rt recurly.NullTime