From 46644404414c73bf0a0a0ce11f8845634871a927 Mon Sep 17 00:00:00 2001 From: Richard Wossal Date: Mon, 12 Feb 2024 16:30:15 +0100 Subject: [PATCH 1/3] Fix: don't let omitempty omit 0.00 prices not very elegant, with the interface{} stuff, but it does work --- invoices.go | 22 +++++++------- invoices_test.go | 78 +++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 85 insertions(+), 15 deletions(-) diff --git a/invoices.go b/invoices.go index c290e83..6113e3a 100644 --- a/invoices.go +++ b/invoices.go @@ -35,7 +35,7 @@ type InvoiceBody struct { TotalPrice InvoiceBodyTotalPrice `json:"totalPrice"` TaxAmounts []InvoiceBodyTaxAmounts `json:"taxAmounts,omitempty"` TaxConditions InvoiceBodyTaxConditions `json:"taxConditions"` - PaymentConditions *InvoiceBodyPaymentConditions `json:"paymentConditions,omitempty"` + PaymentConditions *InvoiceBodyPaymentConditions `json:"paymentConditions,omitempty"` ShippingConditions InvoiceBodyShippingConditions `json:"shippingConditions"` Title string `json:"title,omitempty"` Introduction string `json:"introduction,omitempty"` @@ -58,26 +58,26 @@ type InvoiceBodyLineItems struct { Type string `json:"type"` Name string `json:"name"` Description string `json:"description,omitempty"` - Quantity float64 `json:"quantity,omitempty"` + Quantity interface{} `json:"quantity,omitempty"` UnitName string `json:"unitName,omitempty"` UnitPrice InvoiceBodyUnitPrice `json:"unitPrice,omitempty"` - DiscountPercentage int `json:"discountPercentage,omitempty"` - LineItemAmount float64 `json:"lineItemAmount,omitempty"` + DiscountPercentage interface{} `json:"discountPercentage,omitempty"` + LineItemAmount interface{} `json:"lineItemAmount,omitempty"` } type InvoiceBodyUnitPrice struct { - Currency string `json:"currency"` - NetAmount float64 `json:"netAmount,omitempty"` - GrossAmount float64 `json:"grossAmount,omitempty"` - TaxRatePercentage int `json:"taxRatePercentage"` + Currency string `json:"currency"` + NetAmount interface{} `json:"netAmount,omitempty"` + GrossAmount interface{} `json:"grossAmount,omitempty"` + TaxRatePercentage int `json:"taxRatePercentage"` } type InvoiceBodyTotalPrice struct { Currency string `json:"currency"` - TotalNetAmount float64 `json:"totalNetAmount,omitempty"` - TotalGrossAmount float64 `json:"totalGrossAmount,omitempty"` + TotalNetAmount interface{} `json:"totalNetAmount,omitempty"` + TotalGrossAmount interface{} `json:"totalGrossAmount,omitempty"` TaxRatePercentage interface{} `json:"taxRatePercentage,omitempty"` - TotalTaxAmount float64 `json:"totalTaxAmount,omitempty"` + TotalTaxAmount interface{} `json:"totalTaxAmount,omitempty"` TotalDiscountAbsolute interface{} `json:"totalDiscountAbsolute,omitempty"` TotalDiscountPercentage interface{} `json:"totalDiscountPercentage,omitempty"` } diff --git a/invoices_test.go b/invoices_test.go index f8fc792..968bb72 100644 --- a/invoices_test.go +++ b/invoices_test.go @@ -1,16 +1,86 @@ package golexoffice_test import ( + "encoding/json" "testing" - "encoding/json" "github.com/hostwithquantum/golexoffice" "github.com/stretchr/testify/assert" ) func TestEmptyInvoiceOnlyHasRequiredProperties(t *testing.T) { - body := golexoffice.InvoiceBody{} + body := golexoffice.InvoiceBody{} encoded, err := json.Marshal(body) - assert.NoError(t, err) - assert.Equal(t, `{"voucherDate":"","address":{},"lineItems":null,"totalPrice":{"currency":""},"taxConditions":{"taxType":""},"shippingConditions":{"shippingType":""}}`, string(encoded)) + assert.NoError(t, err) + assert.JSONEq(t, ` + { + "voucherDate": "", + "address": {}, + "lineItems": null, + "totalPrice": { + "currency": "" + }, + "taxConditions": { + "taxType": "" + }, + "shippingConditions": { + "shippingType": "" + } + }`, string(encoded)) +} + +func TestPriceOfZeroIsNotOmitted(t *testing.T) { + body := golexoffice.InvoiceBody{ + TotalPrice: golexoffice.InvoiceBodyTotalPrice{ + TotalGrossAmount: 0, + }, + LineItems: []golexoffice.InvoiceBodyLineItems{ + { + UnitPrice: golexoffice.InvoiceBodyUnitPrice{ + NetAmount: 0.0, + }, + }, + { + UnitPrice: golexoffice.InvoiceBodyUnitPrice{ + GrossAmount: 0.0, + }, + }}, + } + encoded, err := json.Marshal(body) + assert.NoError(t, err) + assert.JSONEq(t, ` + { + "voucherDate": "", + "address": {}, + "lineItems": [ + { + "name": "", + "type": "", + "unitPrice": { + "currency": "", + "taxRatePercentage": 0, + "netAmount": 0 + } + }, + { + "name": "", + "type": "", + "unitPrice": { + "currency": "", + "taxRatePercentage": 0, + "grossAmount": 0 + } + } + ], + "totalPrice": { + "currency": "", + "totalGrossAmount": 0 + }, + "taxConditions": { + "taxType": "" + }, + "shippingConditions": { + "shippingType": "" + } + }`, string(encoded)) } From f016a91a00b32fa2639b84ecf2e8dcef3b69d77c Mon Sep 17 00:00:00 2001 From: Richard Wossal Date: Mon, 12 Feb 2024 17:06:38 +0100 Subject: [PATCH 2/3] Chore: automatically do a few retries on 429 --- config.go | 35 ++++++++++++++++++++++++++--------- errors_test.go | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 9 deletions(-) diff --git a/config.go b/config.go index d9c834c..7267bb4 100644 --- a/config.go +++ b/config.go @@ -16,10 +16,12 @@ import ( "io" "net/http" "strings" + "time" ) const ( - baseURL = "https://api.lexoffice.io" + baseURL = "https://api.lexoffice.io" + maxRateLimitTries = 3 ) // Config is to define the request data @@ -66,15 +68,26 @@ func (c *Config) Send(path string, body io.Reader, method, contentType string) ( request.Header.Set("Content-Type", contentType) request.Header.Set("Accept", "application/json") + var response *http.Response // Send request & get response - response, err := c.client.Do(request) - if err != nil { - return nil, err - } - - if isSuccessful(response) { - // Return data - return response, nil + for i := 1; ; i++ { + response, err = c.client.Do(request) + if err != nil { + return nil, err + } + + if isSuccessful(response) { + // Return data + return response, nil + } else if hitRateLimit(response) { + if i > maxRateLimitTries { + break + } + // max 2 requests per second, so let's wait a bit and try again + time.Sleep(500 * time.Duration(i) * time.Millisecond) + } else { + break + } } // TODO(till): revisit parsing when we add more API endpoints @@ -89,6 +102,10 @@ func isSuccessful(response *http.Response) bool { return response.StatusCode < 400 } +func hitRateLimit(response *http.Response) bool { + return response.StatusCode == 429 +} + func parseErrorResponse(response *http.Response) error { var errorResp ErrorResponse err := json.NewDecoder(response.Body).Decode(&errorResp) diff --git a/errors_test.go b/errors_test.go index 857af7b..af102ba 100644 --- a/errors_test.go +++ b/errors_test.go @@ -67,6 +67,41 @@ func TestErrorNoDetails(t *testing.T) { }) } +func TestRateLimit(t *testing.T) { + rateLimitHits := 10 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if rateLimitHits > 0 { + rateLimitHits -= 1 + w.WriteHeader(http.StatusTooManyRequests) + w.Write([]byte(`{ + "status": 429, + "error": "Too Many Requests", + "message": "Rate limit exceeded" + }`)) + } else { + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{}`)) //nolint:errcheck + } + })) + defer server.Close() + + lexOffice := golexoffice.NewConfig("token", nil) + lexOffice.SetBaseUrl(server.URL) + + t.Run("retry until ok", func(t *testing.T) { + rateLimitHits = 2 + _, err := lexOffice.Invoice("tralalala") + assert.NoError(t, err) + }) + + t.Run("retry until out of retries", func(t *testing.T) { + rateLimitHits = 10 + _, err := lexOffice.Invoice("tralalala") + assert.Error(t, err) + assert.ErrorContains(t, err, "Rate limit exceeded") + }) +} + func errorMock() *httptest.Server { return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/v1/contacts/" { From 76f3fa042e40e110ab37433750c915faf4147574 Mon Sep 17 00:00:00 2001 From: Richard Wossal Date: Mon, 12 Feb 2024 17:07:03 +0100 Subject: [PATCH 3/3] Fix: remove duplicate response body read --- errors_test.go | 1 + invoices.go | 8 -------- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/errors_test.go b/errors_test.go index af102ba..c4c389d 100644 --- a/errors_test.go +++ b/errors_test.go @@ -73,6 +73,7 @@ func TestRateLimit(t *testing.T) { if rateLimitHits > 0 { rateLimitHits -= 1 w.WriteHeader(http.StatusTooManyRequests) + //nolint:errcheck w.Write([]byte(`{ "status": 429, "error": "Too Many Requests", diff --git a/invoices.go b/invoices.go index 6113e3a..cd25199 100644 --- a/invoices.go +++ b/invoices.go @@ -14,8 +14,6 @@ package golexoffice import ( "bytes" "encoding/json" - "fmt" - "io" ) // InvoiceBody is to define body data @@ -134,12 +132,6 @@ func (c *Config) Invoice(id string) (InvoiceBody, error) { // Close request defer response.Body.Close() - read, err := io.ReadAll(response.Body) - if err != nil { - return InvoiceBody{}, err - } - fmt.Println(string(read)) - // Decode data var decode InvoiceBody