Skip to content
Open
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
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/afosto/sendcloud-go
go 1.15

require (
github.com/shopspring/decimal v1.2.0
github.com/stretchr/testify v1.7.0
golang.org/x/text v0.3.5
)
3 changes: 3 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@ github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e h1:FDhOuMEY4JVRztM/gsbk+IKUQ8kj74bxZrgw87eMMVc=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
Expand Down
85 changes: 58 additions & 27 deletions parcel.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package sendcloud
import (
"encoding/json"
"time"

"github.com/shopspring/decimal"
)

type LabelData []byte
Expand Down Expand Up @@ -33,45 +35,52 @@ type ParcelParams struct {
PhoneNumber string
ExternalID string
ToServicePointID int64
Weight string
OrderNumber string
SenderID int64
Items []CreateParcelItemRequest
// The currency of the total order value. Validated against a format of
// “XYZ” (ISO 4217).
TotalOrderValueCurrency *string
// The value paid by the buyer (via various payment methods supported by the
// shop(cash on delivery, pre-paid or post-paid), it will also be used for
// the cash on delivery amount for example “99.99”.
TotalOrderValue *string

// Weight contains the weight in Kilograms. Supports up to 3 decimals
// (grams). For example: decimal.New(20, -3) specifies 20 grams.
Weight decimal.Decimal

@GeertJohan GeertJohan Jun 8, 2021

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This incurs a breaking change in the API of this package. The Weight field was typed as int64. I don't like breaking changes so initially tried to make it backwards compatible but found that it was impossible to do that because previous behavior was unspecified.

The Weight field here was not sent to SendCloud. It was just ignored. There is no documentation on the field that specifies whether it is expected to have the weight in Kilograms, grams, pounds, whatever. So if we were to silently start sending this value to SendCloud it might cause issues with existing users of the package that use a different unit from whatever we choose to convert/send (e.g. they set it in grammes, we send it in Kilos).
Therefore I propose to explicitly break existing users of this package (if they set the Weight field), forcing them to validate their code and explicitly pass a decimal.Decimal value in Kilograms.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch about the in64! We'd like to keep the dependencies of this package to a minimum, so I don't think using an external package to do something a float64 could do is what we're looking for.

You're right that there's no documentation in this package about the type of weight we're expecting. However, the Sendcloud documentation specifies that they need the weight in kilograms (https://docs.sendcloud.sc/api/v2/shipping/#create-a-parcel). It's a great idea to document it in this package like you did here.

Keeping in mind that Sendcloud does specify they want the weight in kilograms, it would be a mistake on the consumer side to send grams in the first place.

Let me know if you have a case against using float64 types rather than decimal.Decimal in this case.

@GeertJohan GeertJohan Aug 3, 2021

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that external dependencies is something to avoid. In this case I needed it because the Sendcloud API places a constraint on the number of decimal places that can be sent. Otherwise it returns this error:

2021/08/03 17:01:02 failed to create a new parcel at sendcloud: request api/v2/parcels resulted in error code 400: weight: "Ensure that there are no more than 3 decimal places."

Also, a float is not an accurate decimal type. It shouldn't be used for things like weight and money/value.

So I would still suggest to include the github.com/shopspring/decimal package as a dependency, it is a popular and well-maintained package.


CustomsInvoiceNr string
CustomsShipmentType int64

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this package should define the possible values as a typed const (enum-esque construction)?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, good idea!

ParcelItems []ParcelParamsItem
TotalOrderValue decimal.Decimal
TotalOrderValueCurrency string
// Shipping method name selected by buyer during the checkout
ShippingMethodCheckoutName *string
// Customs invoice number
CustomsInvoiceNr *string
// Customs shipment type
CustomsShipmentType *CustomsShipmentType
// When set to true configured shipping rules will be applied before creating the label and announcing the Parcel
ApplyShippingRules *bool
}

type ParcelParamsItem struct {
Description string
Quantity uint64
Weight decimal.Decimal

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These could be float64

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @roelofjan-elsinga , sorry for the late reply!

I tried float64 but some API calls failed because four decimal places are not supported (8.1234 kg). This is very easy to occur with floats64's, as they don't enforce the number of decimal-places. The shopsprig/decimal package is very popular, imported by over 30k public Go packages (and probably even more private ones) it's the de-factor decimal package for Golang. I don't think users of the sendcloud package would mind having it as indirect dependency.

Value decimal.Decimal
HsCode string
OriginCountry string
SKU string
}

type CreateParcelItemRequest struct {
// Harmonized System Code Wikipedia Link. Providing a complete HS code with 8 characters increases the delivery rate.
HsCode string `json:"hs_code"`
// Weight of a single item in kilograms.
Weight string `json:"weight"`
Weight decimal.Decimal `json:"weight"`
// Quantity of items shipped.
Quantity int `json:"quantity"`
// Description of the item.
Description string `json:"description"`
// ISO-2 code of the country where the items were originally produced. External Link.
OriginCountry string `json:"origin_country,omitempty"`
// Value of a single item.
Value float64 `json:"value"`
Value decimal.Decimal `json:"value"`
// The SKU of the product.
SKU string `json:"sku,omitempty"`
// External ID of the item generated by a shop system or similar.
ItemId string `json:"item_id,omitempty"`
// The list of properties of the product. Used as a JSON object with {key’: ‘value}.
// The list of properties of the product. Used as a JSON object with {'key': 'value'}.
Properties map[string]interface{} `json:"properties,omitempty"`
}

Expand Down Expand Up @@ -131,15 +140,25 @@ type ParcelRequest struct {
ExternalID *string `json:"external_reference,omitempty"`
SenderID *int64 `json:"sender_address,omitempty"`
Shipment *CreateParcelShipmentRequest `json:"shipment,omitempty"`
Items []CreateParcelItemRequest `json:"parcel_items,omitempty"`
TotalOrderValueCurrency *string `json:"total_order_value_currency,omitempty"`
TotalOrderValue *string `json:"total_order_value,omitempty"`
CustomsInvoiceNr string `json:"customs_invoice_nr"`
CustomsShipmentType int64 `json:"customs_shipment_type"`
ParcelItems []ParcelRequestItem `json:"parcel_items"`
TotalOrderValue string `json:"total_order_value"`
TotalOrderValueCurrency string `json:"total_order_value_currency"`
ShippingMethodCheckoutName *string `json:"shipping_method_checkout_name,omitempty"`
CustomsInvoiceNr *string `json:"customs_invoice_nr,omitempty"`
CustomsShipmentType *CustomsShipmentType `json:"customs_shipment_type,omitempty"`
ApplyShippingRules *bool `json:"apply_shipping_rules,omitempty"`
}

type ParcelRequestItem struct {
Description string `json:"description"`
Quantity uint64 `json:"quantity"`
Weight string `json:"weight"`
Value json.RawMessage `json:"value"` // decimal (as json-number, not as json-string), but we can't go through float64.
HsCode string `json:"hs_code"`
OriginCountry string `json:"origin_country"`
SKU string `json:"sku"`
}

type LabelResponseContainer struct {
Label LabelResponse `json:"label"`
}
Expand Down Expand Up @@ -230,14 +249,16 @@ func (p *ParcelParams) GetPayload() interface{} {
Telephone: p.PhoneNumber,
Email: p.EmailAddress,
RequestLabel: p.IsLabelRequested,
Items: p.Items,
TotalOrderValueCurrency: p.TotalOrderValueCurrency,
TotalOrderValue: p.TotalOrderValue,
ShippingMethodCheckoutName: p.ShippingMethodCheckoutName,
CustomsInvoiceNr: p.CustomsInvoiceNr,
CustomsShipmentType: p.CustomsShipmentType,
TotalOrderValue: p.TotalOrderValue.String(),
TotalOrderValueCurrency: p.TotalOrderValueCurrency,
ShippingMethodCheckoutName: p.ShippingMethodCheckoutName,
ApplyShippingRules: p.ApplyShippingRules,
}
if !p.Weight.Equals(decimal.Zero) {
parcel.Weight = p.Weight.String()
}
if p.Method != 0 {
parcel.Shipment = &CreateParcelShipmentRequest{ID: p.Method}
}
Expand All @@ -254,8 +275,18 @@ func (p *ParcelParams) GetPayload() interface{} {
if p.ToServicePointID != 0 {
parcel.ToServicePointID = &p.ToServicePointID
}
if p.Weight != "" {
parcel.Weight = p.Weight

parcel.ParcelItems = make([]ParcelRequestItem, len(p.ParcelItems))
for i, item := range p.ParcelItems {
parcel.ParcelItems[i] = ParcelRequestItem{
Description: item.Description,
Quantity: item.Quantity,
Weight: item.Weight.String(),
Value: json.RawMessage(item.Value.String()),
HsCode: item.HsCode,
OriginCountry: item.OriginCountry,
SKU: item.SKU,
}
}

ar := ParcelRequestContainer{Parcel: parcel}
Expand Down
5 changes: 3 additions & 2 deletions parcel_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"testing"

"github.com/afosto/sendcloud-go"
"github.com/shopspring/decimal"
"github.com/stretchr/testify/assert"
)

Expand All @@ -20,7 +21,7 @@ func TestGetPayload(t *testing.T) {
{
Name: "Should include weight in request",
Params: sendcloud.ParcelParams{
Weight: "0.040",
Weight: decimal.RequireFromString("0.040"),
},
},
}
Expand All @@ -32,7 +33,7 @@ func TestGetPayload(t *testing.T) {
err := json.Unmarshal(b, &obj)
assert.NoError(t, err)

if test.Params.Weight == "" {
if test.Params.Weight.IsZero() {
_, ok := obj["parcel"]["weight"]
assert.False(t, ok)
}
Expand Down
Loading